Remove dependency errors (#507)

This commit is contained in:
Andrew Morgan 2018-06-18 05:44:25 -07:00 committed by GitHub
parent 06338b06d7
commit a76e31f1b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
242 changed files with 26932 additions and 650 deletions

16
vendor/manifest vendored
View File

@ -298,12 +298,6 @@
"revision": "6a22caf2fd45d5e2119bfc3717e984f15a7eb7ee", "revision": "6a22caf2fd45d5e2119bfc3717e984f15a7eb7ee",
"branch": "master" "branch": "master"
}, },
{
"importpath": "github.com/tj/go-debug",
"repository": "https://github.com/tj/go-debug",
"revision": "ff4a55a20a86994118644bbddc6a216da193cc13",
"branch": "master"
},
{ {
"importpath": "github.com/uber-go/atomic", "importpath": "github.com/uber-go/atomic",
"repository": "https://github.com/uber-go/atomic", "repository": "https://github.com/uber-go/atomic",
@ -311,14 +305,14 @@
"branch": "master" "branch": "master"
}, },
{ {
"importpath": "github.com/uber/jaeger-client-go", "importpath": "github.com/jaegertracing/jaeger-client-go",
"repository": "https://github.com/uber/jaeger-client-go", "repository": "https://github.com/jaegertracing/jaeger-client-go",
"revision": "3ad49a1d839b517923a6fdac36d81cbf7b744f37", "revision": "3ad49a1d839b517923a6fdac36d81cbf7b744f37",
"branch": "master" "branch": "master"
}, },
{ {
"importpath": "github.com/uber/jaeger-lib/metrics", "importpath": "github.com/jaegertracing/jaeger-lib/metrics",
"repository": "https://github.com/uber/jaeger-lib", "repository": "https://github.com/jaegertracing/jaeger-lib",
"revision": "21a3da6d66fe0e278072676fdc84cd4c9ccb9b67", "revision": "21a3da6d66fe0e278072676fdc84cd4c9ccb9b67",
"branch": "master", "branch": "master",
"path": "/metrics" "path": "/metrics"
@ -458,7 +452,7 @@
{ {
"importpath": "gopkg.in/h2non/bimg.v1", "importpath": "gopkg.in/h2non/bimg.v1",
"repository": "https://gopkg.in/h2non/bimg.v1", "repository": "https://gopkg.in/h2non/bimg.v1",
"revision": "45f8993550e71ee7b8001d40c681c6c9fa822357", "revision": "02e621739c77c791d8c153f240b7a1f75b07816f",
"branch": "master" "branch": "master"
}, },
{ {

View File

@ -0,0 +1,131 @@
Changes by Version
==================
2.9.1 (unreleased)
------------------
- nothing yet
2.9.0 (2017-07-29)
------------------
- Pin thrift <= 0.10 (#179)
- Introduce a parallel interface ContribObserver (#159)
2.8.0 (2017-07-05)
------------------
- Drop `jaeger.` prefix from `jaeger.hostname` process-level tag
- Add options to set tracer tags
2.7.0 (2017-06-21)
------------------
- Fix rate limiter balance [#135](https://github.com/uber/jaeger-client-go/pull/135) [#140](https://github.com/uber/jaeger-client-go/pull/140)
- Default client to send Jaeger.thrift [#147](https://github.com/uber/jaeger-client-go/pull/147)
- Save baggage in span [#153](https://github.com/uber/jaeger-client-go/pull/153)
- Move reporter.queueLength to the top of the struct to guarantee 64bit alignment [#158](https://github.com/uber/jaeger-client-go/pull/158)
- Support HTTP transport with jaeger.thrift [#161](https://github.com/uber/jaeger-client-go/pull/161)
2.6.0 (2017-03-28)
------------------
- Add config option to initialize RPC Metrics feature
2.5.0 (2017-03-23)
------------------
- Split request latency metric by success/failure [#123](https://github.com/uber/jaeger-client-go/pull/123)
- Add mutex to adaptive sampler and fix race condition [#124](https://github.com/uber/jaeger-client-go/pull/124)
- Fix rate limiter panic [#125](https://github.com/uber/jaeger-client-go/pull/125)
2.4.0 (2017-03-21)
------------------
- Remove `_ms` suffix from request latency metric name [#121](https://github.com/uber/jaeger-client-go/pull/121)
- Rename all metrics to "request" and "http_request" and use tags for other dimensions [#121](https://github.com/uber/jaeger-client-go/pull/121)
2.3.0 (2017-03-20)
------------------
- Make Span type public to allow access to non-std methods for testing [#117](https://github.com/uber/jaeger-client-go/pull/117)
- Add a structured way to extract traces for logging with zap [#118](https://github.com/uber/jaeger-client-go/pull/118)
2.2.1 (2017-03-14)
------------------
- Fix panic caused by updating the remote sampler from adaptive sampler to any other sampler type (https://github.com/uber/jaeger-client-go/pull/111)
2.2.0 (2017-03-10)
------------------
- Introduce Observer and SpanObserver (https://github.com/uber/jaeger-client-go/pull/94)
- Add RPC metrics emitter as Observer/SpanObserver (https://github.com/uber/jaeger-client-go/pull/103)
2.1.2 (2017-02-27)
-------------------
- Fix leaky bucket bug (https://github.com/uber/jaeger-client-go/pull/99)
- Fix zap logger Infof (https://github.com/uber/jaeger-client-go/pull/100)
- Add tracer initialization godoc examples
2.1.1 (2017-02-21)
-------------------
- Fix inefficient usage of zap.Logger
2.1.0 (2017-02-17)
-------------------
- Add adapter for zap.Logger (https://github.com/uber-go/zap)
- Move logging API to ./log/ package
2.0.0 (2017-02-08)
-------------------
- Support Adaptive Sampling
- Support 128bit Trace IDs
- Change trace/span IDs from uint64 to strong types TraceID and SpanID
- Add Zipkin HTTP B3 Propagation format support #72
- Rip out existing metrics and use github.com/uber/jaeger-lib/metrics
- Change API for tracer, reporter, sampler initialization
1.6.0 (2016-10-14)
-------------------
- Add Zipkin HTTP transport
- Support external baggage via jaeger-baggage header
- Unpin Thrift version, keep to master
1.5.1 (2016-09-27)
-------------------
- Relax dependency on opentracing to ^1
1.5.0 (2016-09-27)
-------------------
- Upgrade to opentracing-go 1.0
- Support KV logging for Spans
1.4.0 (2016-09-14)
-------------------
- Support debug traces via HTTP header "jaeger-debug-id"

View File

@ -0,0 +1,62 @@
# Contributing to `jaeger-client-go`
We'd love your help! If you would like to contribute code you can do so through GitHub
by forking the repository and sending a pull request into the `master` branch.
## Getting Started
This library uses [glide](https://github.com/Masterminds/glide) to manage dependencies.
The library's import path is `github.com/uber/jaeger-client-go`.
To get started:
```bash
git submodule update --init --recursive
glide install
make test
```
## Making A Change
*Before making any significant changes, please [open an
issue](https://github.com/uber/jaeger-client-go/issues).* Discussing your proposed
changes ahead of time will make the contribution process smooth for everyone.
Once we've discussed your changes and you've got your code ready, make sure
that tests are passing (`make test` or `make cover`) and open your PR! Your
pull request is most likely to be accepted if it:
* Includes tests for new functionality.
* Follows the guidelines in [Effective
Go](https://golang.org/doc/effective_go.html) and the [Go team's common code
review comments](https://github.com/golang/go/wiki/CodeReviewComments).
* Has a [good commit
message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
## Cutting a Release
See [RELEASE.md](./RELEASE.md)
## License
By contributing your code, you agree to license your contribution under the terms of the [Apache 2.0 License](LICENSE).
If you are adding a new file it should have a header like below. The easiest way to add such header is to run `make fmt`.
```
// Copyright (c) 2017 Uber Technologies, Inc.
//
// 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,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,104 @@
PROJECT_ROOT=github.com/uber/jaeger-client-go
PACKAGES := $(shell glide novendor | grep -v ./thrift-gen/...)
# all .go files that don't exist in hidden directories
ALL_SRC := $(shell find . -name "*.go" | grep -v -e vendor -e thrift-gen \
-e ".*/\..*" \
-e ".*/_.*" \
-e ".*/mocks.*")
-include crossdock/rules.mk
export GO15VENDOREXPERIMENT=1
RACE=-race
GOTEST=go test -v $(RACE)
GOLINT=golint
GOVET=go vet
GOFMT=gofmt
FMT_LOG=fmt.log
LINT_LOG=lint.log
THRIFT_VER=0.9.3
THRIFT_IMG=thrift:$(THRIFT_VER)
THRIFT=docker run -v "${PWD}:/data" $(THRIFT_IMG) thrift
THRIFT_GO_ARGS=thrift_import="github.com/apache/thrift/lib/go/thrift"
THRIFT_GEN_DIR=thrift-gen
PASS=$(shell printf "\033[32mPASS\033[0m")
FAIL=$(shell printf "\033[31mFAIL\033[0m")
COLORIZE=sed ''/PASS/s//$(PASS)/'' | sed ''/FAIL/s//$(FAIL)/''
.DEFAULT_GOAL := test-and-lint
.PHONY: test-and-lint
test-and-lint: test fmt lint
.PHONY: test
test:
bash -c "set -e; set -o pipefail; $(GOTEST) $(PACKAGES) | $(COLORIZE)"
.PHONY: fmt
fmt:
$(GOFMT) -e -s -l -w $(ALL_SRC)
./scripts/updateLicenses.sh
.PHONY: lint
lint:
$(GOVET) $(PACKAGES)
@cat /dev/null > $(LINT_LOG)
@$(foreach pkg, $(PACKAGES), $(GOLINT) $(pkg) | grep -v crossdock/thrift >> $(LINT_LOG) || true;)
@[ ! -s "$(LINT_LOG)" ] || (echo "Lint Failures" | cat - $(LINT_LOG) && false)
@$(GOFMT) -e -s -l $(ALL_SRC) > $(FMT_LOG)
@[ ! -s "$(FMT_LOG)" ] || (echo "Go Fmt Failures, run 'make fmt'" | cat - $(FMT_LOG) && false)
.PHONY: install
install:
glide --version || go get github.com/Masterminds/glide
glide install
.PHONY: cover
cover:
./scripts/cover.sh $(shell go list $(PACKAGES))
go tool cover -html=cover.out -o cover.html
# This is not part of the regular test target because we don't want to slow it
# down.
.PHONY: test-examples
test-examples:
make -C examples
# TODO at the moment we're not generating tchan_*.go files
thrift: idl-submodule thrift-image
$(THRIFT) -o /data --gen go:$(THRIFT_GO_ARGS) --out /data/$(THRIFT_GEN_DIR) /data/idl/thrift/agent.thrift
sed -i '' 's|"zipkincore"|"$(PROJECT_ROOT)/thrift-gen/zipkincore"|g' $(THRIFT_GEN_DIR)/agent/*.go
$(THRIFT) -o /data --gen go:$(THRIFT_GO_ARGS) --out /data/$(THRIFT_GEN_DIR) /data/idl/thrift/sampling.thrift
$(THRIFT) -o /data --gen go:$(THRIFT_GO_ARGS) --out /data/$(THRIFT_GEN_DIR) /data/idl/thrift/jaeger.thrift
$(THRIFT) -o /data --gen go:$(THRIFT_GO_ARGS) --out /data/$(THRIFT_GEN_DIR) /data/idl/thrift/zipkincore.thrift
$(THRIFT) -o /data --gen go:$(THRIFT_GO_ARGS) --out /data/$(THRIFT_GEN_DIR) /data/idl/thrift/baggage.thrift
rm -rf thrift-gen/*/*-remote
$(THRIFT) -o /data --gen go:$(THRIFT_GO_ARGS) --out /data/crossdock/thrift/ /data/idl/thrift/crossdock/tracetest.thrift
rm -rf crossdock/thrift/*/*-remote
idl-submodule:
git submodule init
git submodule update
thrift-image:
$(THRIFT) -version
.PHONY: install_ci
install_ci: install
go get github.com/wadey/gocovmerge
go get github.com/mattn/goveralls
go get golang.org/x/tools/cmd/cover
go get github.com/golang/lint/golint
.PHONY: test_ci
test_ci:
@./scripts/cover.sh $(shell go list $(PACKAGES))
make lint

View File

@ -0,0 +1,226 @@
[![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![OpenTracing 1.0 Enabled][ot-img]][ot-url]
# Jaeger Bindings for Go OpenTracing API
This is a client side library that implements an
[OpenTracing](http://opentracing.io) Tracer,
with Zipkin-compatible data model.
## Installation
We recommended using a dependency manager like [glide](https://github.com/Masterminds/glide)
and [semantic versioning](http://semver.org/) when including this library into an application.
For example, Jaeger backend imports this library like this:
```yaml
- package: github.com/uber/jaeger-client-go
version: ^2.7.0
```
If you instead want to use the latest version in `master`, you can pull it via `go get`.
Note that during `go get` you may see build errors due to incompatible dependencies, which is why
we recommend using semantic versions for dependencioes. The error may be fixed by running
`make install` (it will install `glide` if you don't have it):
```shell
go get -u github.com/uber/jaeger-client-go/
cd $GOPATH/src/github.com/uber/jaeger-client-go/
git submodule update --init --recursive
make install
```
## Initialization
See tracer initialization examples in [godoc](https://godoc.org/github.com/uber/jaeger-client-go/config#pkg-examples)
and [config/example_test.go](./config/example_test.go).
### Closing the tracer via `io.Closer`
The constructor functions for Jaeger Tracer return the tracer itself and an `io.Closer` instance.
It is recommended to structure your `main()` so that it calls the `Close()` function on the closer
before exiting, e.g.
```go
tracer, closer, err := cfg.New(...)
defer closer.Close()
```
This is especially useful for command-line tools that enable tracing, as well as
for the long-running apps that support graceful shutdown. For example, if your deployment
system sends SIGTERM instead of killing the process and you trap that signal to do a graceful
exit, then having `defer closer.Closer()` ensures that all buffered spans are flushed.
### Metrics & Monitoring
The tracer emits a number of different metrics, defined in
[metrics.go](metrics.go). The monitoring backend is expected to support
tag-based metric names, e.g. instead of `statsd`-style string names
like `counters.my-service.jaeger.spans.started.sampled`, the metrics
are defined by a short name and a collection of key/value tags, for
example: `name:traces, state:started, sampled:true`.
The monitoring backend is represented by the
[StatsReporter](stats_reporter.go) interface. An implementation
of that interface should be passed to the `New` method during
tracer initialization:
```go
stats := // create StatsReporter implementation
tracer := config.Tracing.New("your-service-name", stats)
```
By default, a no-op `NullStatsReporter` is used.
### Logging
The tracer can be configured with an optional logger, which will be
used to log communication errors, or log spans if a logging reporter
option is specified in the configuration. The logging API is abstracted
by the [Logger](logger.go) interface. A logger instance implementing
this interface can be set on the `Config` object before calling the
`New` method.
Besides the [zap](https://github.com/uber-go/zap) implementation
bundled with this package there is also a [go-kit](https://github.com/go-kit/kit)
one in the [jaeger-lib](https://github.com/uber/jaeger-lib) repository.
## Instrumentation for Tracing
Since this tracer is fully compliant with OpenTracing API 1.0,
all code instrumentation should only use the API itself, as described
in the [opentracing-go]
(https://github.com/opentracing/opentracing-go) documentation.
## Features
### Reporters
A "reporter" is a component receives the finished spans and reports
them to somewhere. Under normal circumstances, the Tracer
should use the default `RemoteReporter`, which sends the spans out of
process via configurable "transport". For testing purposes, one can
use an `InMemoryReporter` that accumulates spans in a buffer and
allows to retrieve them for later verification. Also available are
`NullReporter`, a no-op reporter that does nothing, a `LoggingReporter`
which logs all finished spans using their `String()` method, and a
`CompositeReporter` that can be used to combine more than one reporter
into one, e.g. to attach a logging reporter to the main remote reporter.
### Span Reporting Transports
The remote reporter uses "transports" to actually send the spans out
of process. Currently two supported transports are Thrift over UDP
and Thrift over TChannel. More transports will be added in the future.
The only data format currently used is Zipkin Thrift 1.x span format,
which allows easy integration of the tracer with Zipkin backend.
### Sampling
The tracer does not record all spans, but only those that have the
sampling bit set in the `flags`. When a new trace is started and a new
unique ID is generated, a sampling decision is made whether this trace
should be sampled. The sampling decision is propagated to all downstream
calls via the `flags` field of the trace context. The following samplers
are available:
1. `RemotelyControlledSampler` uses one of the other simpler samplers
and periodically updates it by polling an external server. This
allows dynamic control of the sampling strategies.
1. `ConstSampler` always makes the same sampling decision for all
trace IDs. it can be configured to either sample all traces, or
to sample none.
1. `ProbabilisticSampler` uses a fixed sampling rate as a probability
for a given trace to be sampled. The actual decision is made by
comparing the trace ID with a random number multiplied by the
sampling rate.
1. `RateLimitingSampler` can be used to allow only a certain fixed
number of traces to be sampled per second.
### Baggage Injection
The OpenTracing spec allows for [baggage](https://github.com/opentracing/specification/blob/master/specification.md#set-a-baggage-item),
which are key value pairs that are added to the span context and propagated
throughout the trace.
An external process can inject baggage by setting the special
HTTP Header `jaeger-baggage` on a request
```sh
curl -H "jaeger-baggage: key1=value1, key2=value2" http://myhost.com
```
Baggage can also be programatically set inside your service by doing
the following
```go
if span := opentracing.SpanFromContext(ctx); span != nil {
span.SetBaggageItem("key", "value")
}
```
Another service downstream of that can retrieve the baggage in a similar way:
```go
if span := opentracing.SpanFromContext(ctx); span != nil {
val := span.BaggageItem("key")
println(val)
}
```
### Debug Traces (Forced Sampling)
#### Programmatically
The OpenTracing API defines a `sampling.priority` standard tag that
can be used to affect the sampling of a span and its children:
```go
import (
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
)
span := opentracing.SpanFromContext(ctx)
ext.SamplingPriority.Set(span, 1)
```
#### Via HTTP Headers
Jaeger Tracer also understands a special HTTP Header `jaeger-debug-id`,
which can be set in the incoming request, e.g.
```sh
curl -H "jaeger-debug-id: some-correlation-id" http://myhost.com
```
When Jaeger sees this header in the request that otherwise has no
tracing context, it ensures that the new trace started for this
request will be sampled in the "debug" mode (meaning it should survive
all downsampling that might happen in the collection pipeline), and the
root span will have a tag as if this statement was executed:
```go
span.SetTag("jaeger-debug-id", "some-correlation-id")
```
This allows using Jaeger UI to find the trace by this tag.
### Zipkin HTTP B3 compatible header propagation
Jaeger Tracer supports Zipkin B3 Propagation HTTP headers, which are used
by a lot of Zipkin tracers. This means that you can use Jaeger in conjunction with e.g. [these OpenZipkin tracers](https://github.com/openzipkin).
However it is not the default propagation format, see [here](zipkin/README.md#NewZipkinB3HTTPHeaderPropagator) how to set it up.
## License
[Apache 2.0 License](LICENSE).
[doc-img]: https://godoc.org/github.com/uber/jaeger-client-go?status.svg
[doc]: https://godoc.org/github.com/uber/jaeger-client-go
[ci-img]: https://travis-ci.org/uber/jaeger-client-go.svg?branch=master
[ci]: https://travis-ci.org/uber/jaeger-client-go
[cov-img]: https://coveralls.io/repos/uber/jaeger-client-go/badge.svg?branch=master&service=github
[cov]: https://coveralls.io/github/uber/jaeger-client-go?branch=master
[ot-img]: https://img.shields.io/badge/OpenTracing--1.0-enabled-blue.svg
[ot-url]: http://opentracing.io

View File

@ -0,0 +1,11 @@
# Release Process
1. Create a PR "Preparing for release X.Y.Z" against master branch
* Alter CHANGELOG.md from `<placeholder_version> (unreleased)` to `<X.Y.Z> (YYYY-MM-DD)`
* Update `JaegerClientVersion` in constants.go to `Go-X.Y.Z`
2. Create a release "Release X.Y.Z" on Github
* Create Tag `vX.Y.Z`
* Copy CHANGELOG.md into the release notes
3. Create a PR "Back to development" against master branch
* Add `<next_version> (unreleased)` to CHANGELOG.md
* Update `JaegerClientVersion` in constants.go to `Go-<next_version>dev`

View File

@ -0,0 +1,77 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"github.com/opentracing/opentracing-go/log"
"github.com/uber/jaeger-client-go/internal/baggage"
)
// baggageSetter is an actor that can set a baggage value on a Span given certain
// restrictions (eg. maxValueLength).
type baggageSetter struct {
restrictionManager baggage.RestrictionManager
metrics *Metrics
}
func newBaggageSetter(restrictionManager baggage.RestrictionManager, metrics *Metrics) *baggageSetter {
return &baggageSetter{
restrictionManager: restrictionManager,
metrics: metrics,
}
}
// (NB) span should hold the lock before making this call
func (s *baggageSetter) setBaggage(span *Span, key, value string) {
var truncated bool
var prevItem string
restriction := s.restrictionManager.GetRestriction(span.serviceName(), key)
if !restriction.KeyAllowed() {
s.logFields(span, key, value, prevItem, truncated, restriction.KeyAllowed())
s.metrics.BaggageUpdateFailure.Inc(1)
return
}
if len(value) > restriction.MaxValueLength() {
truncated = true
value = value[:restriction.MaxValueLength()]
s.metrics.BaggageTruncate.Inc(1)
}
prevItem = span.context.baggage[key]
s.logFields(span, key, value, prevItem, truncated, restriction.KeyAllowed())
span.context = span.context.WithBaggageItem(key, value)
s.metrics.BaggageUpdateSuccess.Inc(1)
}
func (s *baggageSetter) logFields(span *Span, key, value, prevItem string, truncated, valid bool) {
if !span.context.IsSampled() {
return
}
fields := []log.Field{
log.String("event", "baggage"),
log.String("key", key),
log.String("value", value),
}
if prevItem != "" {
fields = append(fields, log.String("override", "true"))
}
if truncated {
fields = append(fields, log.String("truncated", "true"))
}
if !valid {
fields = append(fields, log.String("invalid", "true"))
}
span.logFieldsNoLocking(fields...)
}

View File

@ -0,0 +1,126 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"testing"
"github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-lib/metrics/testutils"
"github.com/uber/jaeger-client-go/internal/baggage"
)
func withTracerAndMetrics(f func(tracer *Tracer, metrics *Metrics, factory *metrics.LocalFactory)) {
factory := metrics.NewLocalFactory(0)
m := NewMetrics(factory, nil)
service := "DOOP"
tracer, closer := NewTracer(service, NewConstSampler(true), NewNullReporter())
defer closer.Close()
f(tracer.(*Tracer), m, factory)
}
func TestTruncateBaggage(t *testing.T) {
withTracerAndMetrics(func(tracer *Tracer, metrics *Metrics, factory *metrics.LocalFactory) {
setter := newBaggageSetter(baggage.NewDefaultRestrictionManager(5), metrics)
key := "key"
value := "01234567890"
expected := "01234"
parent := tracer.StartSpan("parent").(*Span)
parent.context = parent.context.WithBaggageItem(key, value)
span := tracer.StartSpan("child", opentracing.ChildOf(parent.Context())).(*Span)
setter.setBaggage(span, key, value)
assertBaggageFields(t, span, key, expected, true, true, false)
assert.Equal(t, expected, span.context.baggage[key])
testutils.AssertCounterMetrics(t, factory,
testutils.ExpectedMetric{
Name: "jaeger.baggage-truncate",
Value: 1,
},
testutils.ExpectedMetric{
Name: "jaeger.baggage-update",
Tags: map[string]string{"result": "ok"},
Value: 1,
},
)
})
}
type keyNotAllowedBaggageRestrictionManager struct{}
func (m *keyNotAllowedBaggageRestrictionManager) GetRestriction(service, key string) *baggage.Restriction {
return baggage.NewRestriction(false, 0)
}
func TestInvalidBaggage(t *testing.T) {
withTracerAndMetrics(func(tracer *Tracer, metrics *Metrics, factory *metrics.LocalFactory) {
setter := newBaggageSetter(&keyNotAllowedBaggageRestrictionManager{}, metrics)
key := "key"
value := "value"
span := tracer.StartSpan("span").(*Span)
setter.setBaggage(span, key, value)
assertBaggageFields(t, span, key, value, false, false, true)
assert.Empty(t, span.context.baggage[key])
testutils.AssertCounterMetrics(t, factory,
testutils.ExpectedMetric{
Name: "jaeger.baggage-update",
Tags: map[string]string{"result": "err"},
Value: 1,
},
)
})
}
func TestNotSampled(t *testing.T) {
withTracerAndMetrics(func(_ *Tracer, metrics *Metrics, factory *metrics.LocalFactory) {
tracer, closer := NewTracer("svc", NewConstSampler(false), NewNullReporter())
defer closer.Close()
setter := newBaggageSetter(baggage.NewDefaultRestrictionManager(10), metrics)
span := tracer.StartSpan("span").(*Span)
setter.setBaggage(span, "key", "value")
assert.Empty(t, span.logs, "No baggage fields should be created if span is not sampled")
})
}
func assertBaggageFields(t *testing.T, sp *Span, key, value string, override, truncated, invalid bool) {
require.Len(t, sp.logs, 1)
keys := map[string]struct{}{}
for _, field := range sp.logs[0].Fields {
keys[field.String()] = struct{}{}
}
assert.Contains(t, keys, "event:baggage")
assert.Contains(t, keys, "key:"+key)
assert.Contains(t, keys, "value:"+value)
if invalid {
assert.Contains(t, keys, "invalid:true")
}
if override {
assert.Contains(t, keys, "override:true")
}
if truncated {
assert.Contains(t, keys, "truncated:true")
}
}

View File

@ -0,0 +1,287 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/internal/baggage/remote"
"github.com/uber/jaeger-client-go/rpcmetrics"
)
const defaultSamplingProbability = 0.001
// Configuration configures and creates Jaeger Tracer
type Configuration struct {
Disabled bool `yaml:"disabled"`
Sampler *SamplerConfig `yaml:"sampler"`
Reporter *ReporterConfig `yaml:"reporter"`
Headers *jaeger.HeadersConfig `yaml:"headers"`
RPCMetrics bool `yaml:"rpc_metrics"`
BaggageRestrictions *BaggageRestrictionsConfig `yaml:"baggage_restrictions"`
}
// SamplerConfig allows initializing a non-default sampler. All fields are optional.
type SamplerConfig struct {
// Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote
Type string `yaml:"type"`
// Param is a value passed to the sampler.
// Valid values for Param field are:
// - for "const" sampler, 0 or 1 for always false/true respectively
// - for "probabilistic" sampler, a probability between 0 and 1
// - for "rateLimiting" sampler, the number of spans per second
// - for "remote" sampler, param is the same as for "probabilistic"
// and indicates the initial sampling rate before the actual one
// is received from the mothership
Param float64 `yaml:"param"`
// SamplingServerURL is the address of jaeger-agent's HTTP sampling server
SamplingServerURL string `yaml:"samplingServerURL"`
// MaxOperations is the maximum number of operations that the sampler
// will keep track of. If an operation is not tracked, a default probabilistic
// sampler will be used rather than the per operation specific sampler.
MaxOperations int `yaml:"maxOperations"`
// SamplingRefreshInterval controls how often the remotely controlled sampler will poll
// jaeger-agent for the appropriate sampling strategy.
SamplingRefreshInterval time.Duration `yaml:"samplingRefreshInterval"`
}
// ReporterConfig configures the reporter. All fields are optional.
type ReporterConfig struct {
// QueueSize controls how many spans the reporter can keep in memory before it starts dropping
// new spans. The queue is continuously drained by a background go-routine, as fast as spans
// can be sent out of process.
QueueSize int `yaml:"queueSize"`
// BufferFlushInterval controls how often the buffer is force-flushed, even if it's not full.
// It is generally not useful, as it only matters for very low traffic services.
BufferFlushInterval time.Duration
// LogSpans, when true, enables LoggingReporter that runs in parallel with the main reporter
// and logs all submitted spans. Main Configuration.Logger must be initialized in the code
// for this option to have any effect.
LogSpans bool `yaml:"logSpans"`
// LocalAgentHostPort instructs reporter to send spans to jaeger-agent at this address
LocalAgentHostPort string `yaml:"localAgentHostPort"`
}
// BaggageRestrictionsConfig configures the baggage restrictions manager which can be used to whitelist
// certain baggage keys. All fields are optional.
type BaggageRestrictionsConfig struct {
// DenyBaggageOnInitializationFailure controls the startup failure mode of the baggage restriction
// manager. If true, the manager will not allow any baggage to be written until baggage restrictions have
// been retrieved from jaeger-agent. If false, the manager wil allow any baggage to be written until baggage
// restrictions have been retrieved from jaeger-agent.
DenyBaggageOnInitializationFailure bool `yaml:"denyBaggageOnInitializationFailure"`
// HostPort is the hostPort of jaeger-agent's baggage restrictions server
HostPort string `yaml:"hostPort"`
// RefreshInterval controls how often the baggage restriction manager will poll
// jaeger-agent for the most recent baggage restrictions.
RefreshInterval time.Duration `yaml:"refreshInterval"`
}
type nullCloser struct{}
func (*nullCloser) Close() error { return nil }
// New creates a new Jaeger Tracer, and a closer func that can be used to flush buffers
// before shutdown.
func (c Configuration) New(
serviceName string,
options ...Option,
) (opentracing.Tracer, io.Closer, error) {
if serviceName == "" {
return nil, nil, errors.New("no service name provided")
}
if c.Disabled {
return &opentracing.NoopTracer{}, &nullCloser{}, nil
}
opts := applyOptions(options...)
tracerMetrics := jaeger.NewMetrics(opts.metrics, nil)
if c.RPCMetrics {
Observer(
rpcmetrics.NewObserver(
opts.metrics.Namespace("jaeger-rpc", map[string]string{"component": "jaeger"}),
rpcmetrics.DefaultNameNormalizer,
),
)(&opts) // adds to c.observers
}
if c.Sampler == nil {
c.Sampler = &SamplerConfig{
Type: jaeger.SamplerTypeRemote,
Param: defaultSamplingProbability,
}
}
if c.Reporter == nil {
c.Reporter = &ReporterConfig{}
}
sampler, err := c.Sampler.NewSampler(serviceName, tracerMetrics)
if err != nil {
return nil, nil, err
}
reporter := opts.reporter
if reporter == nil {
r, err := c.Reporter.NewReporter(serviceName, tracerMetrics, opts.logger)
if err != nil {
return nil, nil, err
}
reporter = r
}
tracerOptions := []jaeger.TracerOption{
jaeger.TracerOptions.Metrics(tracerMetrics),
jaeger.TracerOptions.Logger(opts.logger),
jaeger.TracerOptions.CustomHeaderKeys(c.Headers),
jaeger.TracerOptions.Gen128Bit(opts.gen128Bit),
jaeger.TracerOptions.ZipkinSharedRPCSpan(opts.zipkinSharedRPCSpan),
}
for _, tag := range opts.tags {
tracerOptions = append(tracerOptions, jaeger.TracerOptions.Tag(tag.Key, tag.Value))
}
for _, obs := range opts.observers {
tracerOptions = append(tracerOptions, jaeger.TracerOptions.Observer(obs))
}
for _, cobs := range opts.contribObservers {
tracerOptions = append(tracerOptions, jaeger.TracerOptions.ContribObserver(cobs))
}
if c.BaggageRestrictions != nil {
mgr := remote.NewRestrictionManager(
serviceName,
remote.Options.Metrics(tracerMetrics),
remote.Options.Logger(opts.logger),
remote.Options.HostPort(c.BaggageRestrictions.HostPort),
remote.Options.RefreshInterval(c.BaggageRestrictions.RefreshInterval),
remote.Options.DenyBaggageOnInitializationFailure(
c.BaggageRestrictions.DenyBaggageOnInitializationFailure,
),
)
tracerOptions = append(tracerOptions, jaeger.TracerOptions.BaggageRestrictionManager(mgr))
}
tracer, closer := jaeger.NewTracer(
serviceName,
sampler,
reporter,
tracerOptions...)
return tracer, closer, nil
}
// InitGlobalTracer creates a new Jaeger Tracer, and sets it as global OpenTracing Tracer.
// It returns a closer func that can be used to flush buffers before shutdown.
func (c Configuration) InitGlobalTracer(
serviceName string,
options ...Option,
) (io.Closer, error) {
if c.Disabled {
return &nullCloser{}, nil
}
tracer, closer, err := c.New(serviceName, options...)
if err != nil {
return nil, err
}
opentracing.InitGlobalTracer(tracer)
return closer, nil
}
// NewSampler creates a new sampler based on the configuration
func (sc *SamplerConfig) NewSampler(
serviceName string,
metrics *jaeger.Metrics,
) (jaeger.Sampler, error) {
samplerType := strings.ToLower(sc.Type)
if samplerType == jaeger.SamplerTypeConst {
return jaeger.NewConstSampler(sc.Param != 0), nil
}
if samplerType == jaeger.SamplerTypeProbabilistic {
if sc.Param >= 0 && sc.Param <= 1.0 {
return jaeger.NewProbabilisticSampler(sc.Param)
}
return nil, fmt.Errorf(
"Invalid Param for probabilistic sampler: %v. Expecting value between 0 and 1",
sc.Param,
)
}
if samplerType == jaeger.SamplerTypeRateLimiting {
return jaeger.NewRateLimitingSampler(sc.Param), nil
}
if samplerType == jaeger.SamplerTypeRemote || sc.Type == "" {
sc2 := *sc
sc2.Type = jaeger.SamplerTypeProbabilistic
initSampler, err := sc2.NewSampler(serviceName, nil)
if err != nil {
return nil, err
}
options := []jaeger.SamplerOption{
jaeger.SamplerOptions.Metrics(metrics),
jaeger.SamplerOptions.InitialSampler(initSampler),
jaeger.SamplerOptions.SamplingServerURL(sc.SamplingServerURL),
}
if sc.MaxOperations != 0 {
options = append(options, jaeger.SamplerOptions.MaxOperations(sc.MaxOperations))
}
if sc.SamplingRefreshInterval != 0 {
options = append(options, jaeger.SamplerOptions.SamplingRefreshInterval(sc.SamplingRefreshInterval))
}
return jaeger.NewRemotelyControlledSampler(serviceName, options...), nil
}
return nil, fmt.Errorf("Unknown sampler type %v", sc.Type)
}
// NewReporter instantiates a new reporter that submits spans to tcollector
func (rc *ReporterConfig) NewReporter(
serviceName string,
metrics *jaeger.Metrics,
logger jaeger.Logger,
) (jaeger.Reporter, error) {
sender, err := rc.newTransport()
if err != nil {
return nil, err
}
reporter := jaeger.NewRemoteReporter(
sender,
jaeger.ReporterOptions.QueueSize(rc.QueueSize),
jaeger.ReporterOptions.BufferFlushInterval(rc.BufferFlushInterval),
jaeger.ReporterOptions.Logger(logger),
jaeger.ReporterOptions.Metrics(metrics))
if rc.LogSpans && logger != nil {
logger.Infof("Initializing logging reporter\n")
reporter = jaeger.NewCompositeReporter(jaeger.NewLoggingReporter(logger), reporter)
}
return reporter, err
}
func (rc *ReporterConfig) newTransport() (jaeger.Transport, error) {
return jaeger.NewUDPTransport(rc.LocalAgentHostPort, 0)
}

View File

@ -0,0 +1,259 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"testing"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-lib/metrics/testutils"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/log"
)
func TestNewSamplerConst(t *testing.T) {
constTests := []struct {
param float64
decision bool
}{{1, true}, {0, false}}
for _, tst := range constTests {
cfg := &SamplerConfig{Type: jaeger.SamplerTypeConst, Param: tst.param}
s, err := cfg.NewSampler("x", nil)
require.NoError(t, err)
s1, ok := s.(*jaeger.ConstSampler)
require.True(t, ok, "converted to constSampler")
require.Equal(t, tst.decision, s1.Decision, "decision")
}
}
func TestNewSamplerProbabilistic(t *testing.T) {
constTests := []struct {
param float64
error bool
}{{1.5, true}, {0.5, false}}
for _, tst := range constTests {
cfg := &SamplerConfig{Type: jaeger.SamplerTypeProbabilistic, Param: tst.param}
s, err := cfg.NewSampler("x", nil)
if tst.error {
require.Error(t, err)
} else {
require.NoError(t, err)
_, ok := s.(*jaeger.ProbabilisticSampler)
require.True(t, ok, "converted to ProbabilisticSampler")
}
}
}
func TestDefaultSampler(t *testing.T) {
cfg := Configuration{
Sampler: &SamplerConfig{Type: "InvalidType"},
}
_, _, err := cfg.New("testService")
require.Error(t, err)
}
func TestInvalidSamplerType(t *testing.T) {
cfg := &SamplerConfig{MaxOperations: 10}
s, err := cfg.NewSampler("x", jaeger.NewNullMetrics())
require.NoError(t, err)
rcs, ok := s.(*jaeger.RemotelyControlledSampler)
require.True(t, ok, "converted to RemotelyControlledSampler")
rcs.Close()
}
func TestDefaultConfig(t *testing.T) {
cfg := Configuration{}
_, _, err := cfg.New("", Metrics(metrics.NullFactory), Logger(log.NullLogger))
require.EqualError(t, err, "no service name provided")
_, closer, err := cfg.New("testService")
defer closer.Close()
require.NoError(t, err)
}
func TestDisabledFlag(t *testing.T) {
cfg := Configuration{Disabled: true}
_, closer, err := cfg.New("testService")
defer closer.Close()
require.NoError(t, err)
}
func TestNewReporterError(t *testing.T) {
cfg := Configuration{
Reporter: &ReporterConfig{LocalAgentHostPort: "bad_local_agent"},
}
_, _, err := cfg.New("testService")
require.Error(t, err)
}
func TestInitGlobalTracer(t *testing.T) {
// Save the existing GlobalTracer and replace after finishing function
prevTracer := opentracing.GlobalTracer()
defer opentracing.InitGlobalTracer(prevTracer)
noopTracer := opentracing.NoopTracer{}
tests := []struct {
cfg Configuration
shouldErr bool
tracerChanged bool
}{
{
cfg: Configuration{Disabled: true},
shouldErr: false,
tracerChanged: false,
},
{
cfg: Configuration{Sampler: &SamplerConfig{Type: "InvalidType"}},
shouldErr: true,
tracerChanged: false,
},
{
cfg: Configuration{
Sampler: &SamplerConfig{
Type: "remote",
SamplingRefreshInterval: 1,
},
},
shouldErr: false,
tracerChanged: true,
},
{
cfg: Configuration{},
shouldErr: false,
tracerChanged: true,
},
}
for _, test := range tests {
opentracing.InitGlobalTracer(noopTracer)
_, err := test.cfg.InitGlobalTracer("testService")
if test.shouldErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if test.tracerChanged {
require.NotEqual(t, noopTracer, opentracing.GlobalTracer())
} else {
require.Equal(t, noopTracer, opentracing.GlobalTracer())
}
}
}
func TestConfigWithReporter(t *testing.T) {
c := Configuration{
Sampler: &SamplerConfig{
Type: "const",
Param: 1,
},
}
r := jaeger.NewInMemoryReporter()
tracer, closer, err := c.New("test", Reporter(r))
require.NoError(t, err)
defer closer.Close()
tracer.StartSpan("test").Finish()
assert.Len(t, r.GetSpans(), 1)
}
func TestConfigWithRPCMetrics(t *testing.T) {
metrics := metrics.NewLocalFactory(0)
c := Configuration{
Sampler: &SamplerConfig{
Type: "const",
Param: 1,
},
RPCMetrics: true,
}
r := jaeger.NewInMemoryReporter()
tracer, closer, err := c.New(
"test",
Reporter(r),
Metrics(metrics),
ContribObserver(fakeContribObserver{}),
)
require.NoError(t, err)
defer closer.Close()
tracer.StartSpan("test", ext.SpanKindRPCServer).Finish()
testutils.AssertCounterMetrics(t, metrics,
testutils.ExpectedMetric{
Name: "jaeger-rpc.requests",
Tags: map[string]string{"component": "jaeger", "endpoint": "test", "error": "false"},
Value: 1,
},
)
}
func TestBaggageRestrictionsConfig(t *testing.T) {
m := metrics.NewLocalFactory(0)
c := Configuration{
BaggageRestrictions: &BaggageRestrictionsConfig{
HostPort: "not:1929213",
RefreshInterval: time.Minute,
},
}
_, closer, err := c.New(
"test",
Metrics(m),
)
require.NoError(t, err)
defer closer.Close()
metricName := "jaeger.baggage-restrictions-update"
metricTags := map[string]string{"result": "err"}
key := metrics.GetKey(metricName, metricTags, "|", "=")
for i := 0; i < 100; i++ {
// wait until the async initialization call is complete
counters, _ := m.Snapshot()
if _, ok := counters[key]; ok {
break
}
time.Sleep(time.Millisecond)
}
testutils.AssertCounterMetrics(t, m,
testutils.ExpectedMetric{
Name: metricName,
Tags: metricTags,
Value: 1,
},
)
}
func TestConfigWithGen128Bit(t *testing.T) {
c := Configuration{
Sampler: &SamplerConfig{
Type: "const",
Param: 1,
},
RPCMetrics: true,
}
tracer, closer, err := c.New("test", Gen128Bit(true))
require.NoError(t, err)
defer closer.Close()
span := tracer.StartSpan("test")
defer span.Finish()
traceID := span.Context().(jaeger.SpanContext).TraceID()
require.True(t, traceID.High != 0)
require.True(t, traceID.Low != 0)
}

View File

@ -0,0 +1,84 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config_test
import (
"log"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
jaegerlog "github.com/uber/jaeger-client-go/log"
)
func ExampleConfiguration_InitGlobalTracer_testing() {
// Sample configuration for testing. Use constant sampling to sample every trace
// and enable LogSpan to log every span via configured Logger.
cfg := jaegercfg.Configuration{
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
},
}
// Example logger and metrics factory. Use github.com/uber/jaeger-client-go/log
// and github.com/uber/jaeger-lib/metrics respectively to bind to real logging and metrics
// frameworks.
jLogger := jaegerlog.StdLogger
jMetricsFactory := metrics.NullFactory
// Initialize tracer with a logger and a metrics factory
closer, err := cfg.InitGlobalTracer(
"serviceName",
jaegercfg.Logger(jLogger),
jaegercfg.Metrics(jMetricsFactory),
)
if err != nil {
log.Printf("Could not initialize jaeger tracer: %s", err.Error())
return
}
defer closer.Close()
// continue main()
}
func ExampleConfiguration_InitGlobalTracer_production() {
// Recommended configuration for production.
cfg := jaegercfg.Configuration{}
// Example logger and metrics factory. Use github.com/uber/jaeger-client-go/log
// and github.com/uber/jaeger-lib/metrics respectively to bind to real logging and metrics
// frameworks.
jLogger := jaegerlog.StdLogger
jMetricsFactory := metrics.NullFactory
// Initialize tracer with a logger and a metrics factory
closer, err := cfg.InitGlobalTracer(
"serviceName",
jaegercfg.Logger(jLogger),
jaegercfg.Metrics(jMetricsFactory),
)
if err != nil {
log.Printf("Could not initialize jaeger tracer: %s", err.Error())
return
}
defer closer.Close()
// continue main()
}

View File

@ -0,0 +1,113 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
opentracing "github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-client-go"
)
// Option is a function that sets some option on the client.
type Option func(c *Options)
// Options control behavior of the client.
type Options struct {
metrics metrics.Factory
logger jaeger.Logger
reporter jaeger.Reporter
contribObservers []jaeger.ContribObserver
observers []jaeger.Observer
gen128Bit bool
zipkinSharedRPCSpan bool
tags []opentracing.Tag
}
// Metrics creates an Option that initializes Metrics in the tracer,
// which is used to emit statistics about spans.
func Metrics(factory metrics.Factory) Option {
return func(c *Options) {
c.metrics = factory
}
}
// Logger can be provided to log Reporter errors, as well as to log spans
// if Reporter.LogSpans is set to true.
func Logger(logger jaeger.Logger) Option {
return func(c *Options) {
c.logger = logger
}
}
// Reporter can be provided explicitly to override the configuration.
// Useful for testing, e.g. by passing InMemoryReporter.
func Reporter(reporter jaeger.Reporter) Option {
return func(c *Options) {
c.reporter = reporter
}
}
// Observer can be registered with the Tracer to receive notifications about new Spans.
func Observer(observer jaeger.Observer) Option {
return func(c *Options) {
c.observers = append(c.observers, observer)
}
}
// ContribObserver can be registered with the Tracer to recieve notifications
// about new spans.
func ContribObserver(observer jaeger.ContribObserver) Option {
return func(c *Options) {
c.contribObservers = append(c.contribObservers, observer)
}
}
// Gen128Bit specifies whether to generate 128bit trace IDs.
func Gen128Bit(gen128Bit bool) Option {
return func(c *Options) {
c.gen128Bit = gen128Bit
}
}
// ZipkinSharedRPCSpan creates an option that enables sharing span ID between client
// and server spans a la zipkin. If false, client and server spans will be assigned
// different IDs.
func ZipkinSharedRPCSpan(zipkinSharedRPCSpan bool) Option {
return func(c *Options) {
c.zipkinSharedRPCSpan = zipkinSharedRPCSpan
}
}
// Tag creates an option that adds a tracer-level tag.
func Tag(key string, value interface{}) Option {
return func(c *Options) {
c.tags = append(c.tags, opentracing.Tag{Key: key, Value: value})
}
}
func applyOptions(options ...Option) Options {
opts := Options{}
for _, option := range options {
option(&opts)
}
if opts.metrics == nil {
opts.metrics = metrics.NullFactory
}
if opts.logger == nil {
opts.logger = jaeger.NullLogger
}
return opts
}

View File

@ -0,0 +1,72 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"testing"
opentracing "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-client-go"
)
func TestApplyOptions(t *testing.T) {
metricsFactory := metrics.NewLocalFactory(0)
observer := fakeObserver{}
contribObserver := fakeContribObserver{}
opts := applyOptions(
Metrics(metricsFactory),
Logger(jaeger.StdLogger),
Observer(observer),
ContribObserver(contribObserver),
Gen128Bit(true),
ZipkinSharedRPCSpan(true),
)
assert.Equal(t, jaeger.StdLogger, opts.logger)
assert.Equal(t, metricsFactory, opts.metrics)
assert.Equal(t, []jaeger.Observer{observer}, opts.observers)
assert.Equal(t, []jaeger.ContribObserver{contribObserver}, opts.contribObservers)
assert.True(t, opts.gen128Bit)
assert.True(t, opts.zipkinSharedRPCSpan)
}
func TestTraceTagOption(t *testing.T) {
c := Configuration{}
tracer, closer, err := c.New("test-service", Tag("tag-key", "tag-value"))
require.NoError(t, err)
defer closer.Close()
assert.Equal(t, opentracing.Tag{Key: "tag-key", Value: "tag-value"}, tracer.(*jaeger.Tracer).Tags()[0])
}
func TestApplyOptionsDefaults(t *testing.T) {
opts := applyOptions()
assert.Equal(t, jaeger.NullLogger, opts.logger)
assert.Equal(t, metrics.NullFactory, opts.metrics)
}
type fakeObserver struct{}
func (o fakeObserver) OnStartSpan(operationName string, options opentracing.StartSpanOptions) jaeger.SpanObserver {
return nil
}
type fakeContribObserver struct{}
func (o fakeContribObserver) OnStartSpan(span opentracing.Span, operationName string, options opentracing.StartSpanOptions) (jaeger.ContribSpanObserver, bool) {
return nil, false
}

View File

@ -0,0 +1,76 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
const (
// JaegerClientVersion is the version of the client library reported as Span tag.
JaegerClientVersion = "Go-2.9.1dev"
// JaegerClientVersionTagKey is the name of the tag used to report client version.
JaegerClientVersionTagKey = "jaeger.version"
// JaegerDebugHeader is the name of HTTP header or a TextMap carrier key which,
// if found in the carrier, forces the trace to be sampled as "debug" trace.
// The value of the header is recorded as the tag on the root span, so that the
// trace can be found in the UI using this value as a correlation ID.
JaegerDebugHeader = "jaeger-debug-id"
// JaegerBaggageHeader is the name of the HTTP header that is used to submit baggage.
// It differs from TraceBaggageHeaderPrefix in that it can be used only in cases where
// a root span does not exist.
JaegerBaggageHeader = "jaeger-baggage"
// TracerHostnameTagKey used to report host name of the process.
TracerHostnameTagKey = "hostname"
// TracerIPTagKey used to report ip of the process.
TracerIPTagKey = "ip"
// SamplerTypeTagKey reports which sampler was used on the root span.
SamplerTypeTagKey = "sampler.type"
// SamplerParamTagKey reports the parameter of the sampler, like sampling probability.
SamplerParamTagKey = "sampler.param"
// TraceContextHeaderName is the http header name used to propagate tracing context.
// This must be in lower-case to avoid mismatches when decoding incoming headers.
TraceContextHeaderName = "uber-trace-id"
// TracerStateHeaderName is deprecated.
// Deprecated: use TraceContextHeaderName
TracerStateHeaderName = TraceContextHeaderName
// TraceBaggageHeaderPrefix is the prefix for http headers used to propagate baggage.
// This must be in lower-case to avoid mismatches when decoding incoming headers.
TraceBaggageHeaderPrefix = "uberctx-"
// SamplerTypeConst is the type of sampler that always makes the same decision.
SamplerTypeConst = "const"
// SamplerTypeRemote is the type of sampler that polls Jaeger agent for sampling strategy.
SamplerTypeRemote = "remote"
// SamplerTypeProbabilistic is the type of sampler that samples traces
// with a certain fixed probability.
SamplerTypeProbabilistic = "probabilistic"
// SamplerTypeRateLimiting is the type of sampler that samples
// only up to a fixed number of traces per second.
SamplerTypeRateLimiting = "ratelimiting"
// SamplerTypeLowerBound is the type of sampler that samples
// only up to a fixed number of traces per second.
SamplerTypeLowerBound = "lowerbound"
)

View File

@ -0,0 +1,29 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"strings"
"testing"
)
func TestHeaderConstants(t *testing.T) {
if TraceContextHeaderName != strings.ToLower(TraceContextHeaderName) {
t.Errorf("TraceContextHeaderName is not lower-case: %+v", TraceContextHeaderName)
}
if TraceBaggageHeaderPrefix != strings.ToLower(TraceBaggageHeaderPrefix) {
t.Errorf("TraceBaggageHeaderPrefix is not lower-case: %+v", TraceBaggageHeaderPrefix)
}
}

View File

@ -0,0 +1,258 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"errors"
"fmt"
"strconv"
"strings"
)
const (
flagSampled = byte(1)
flagDebug = byte(2)
)
var (
errEmptyTracerStateString = errors.New("Cannot convert empty string to tracer state")
errMalformedTracerStateString = errors.New("String does not match tracer state format")
emptyContext = SpanContext{}
)
// TraceID represents unique 128bit identifier of a trace
type TraceID struct {
High, Low uint64
}
// SpanID represents unique 64bit identifier of a span
type SpanID uint64
// SpanContext represents propagated span identity and state
type SpanContext struct {
// traceID represents globally unique ID of the trace.
// Usually generated as a random number.
traceID TraceID
// spanID represents span ID that must be unique within its trace,
// but does not have to be globally unique.
spanID SpanID
// parentID refers to the ID of the parent span.
// Should be 0 if the current span is a root span.
parentID SpanID
// flags is a bitmap containing such bits as 'sampled' and 'debug'.
flags byte
// Distributed Context baggage. The is a snapshot in time.
baggage map[string]string
// debugID can be set to some correlation ID when the context is being
// extracted from a TextMap carrier.
//
// See JaegerDebugHeader in constants.go
debugID string
}
// ForeachBaggageItem implements ForeachBaggageItem() of opentracing.SpanContext
func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) {
for k, v := range c.baggage {
if !handler(k, v) {
break
}
}
}
// IsSampled returns whether this trace was chosen for permanent storage
// by the sampling mechanism of the tracer.
func (c SpanContext) IsSampled() bool {
return (c.flags & flagSampled) == flagSampled
}
// IsDebug indicates whether sampling was explicitly requested by the service.
func (c SpanContext) IsDebug() bool {
return (c.flags & flagDebug) == flagDebug
}
// IsValid indicates whether this context actually represents a valid trace.
func (c SpanContext) IsValid() bool {
return c.traceID.IsValid() && c.spanID != 0
}
func (c SpanContext) String() string {
if c.traceID.High == 0 {
return fmt.Sprintf("%x:%x:%x:%x", c.traceID.Low, uint64(c.spanID), uint64(c.parentID), c.flags)
}
return fmt.Sprintf("%x%016x:%x:%x:%x", c.traceID.High, c.traceID.Low, uint64(c.spanID), uint64(c.parentID), c.flags)
}
// ContextFromString reconstructs the Context encoded in a string
func ContextFromString(value string) (SpanContext, error) {
var context SpanContext
if value == "" {
return emptyContext, errEmptyTracerStateString
}
parts := strings.Split(value, ":")
if len(parts) != 4 {
return emptyContext, errMalformedTracerStateString
}
var err error
if context.traceID, err = TraceIDFromString(parts[0]); err != nil {
return emptyContext, err
}
if context.spanID, err = SpanIDFromString(parts[1]); err != nil {
return emptyContext, err
}
if context.parentID, err = SpanIDFromString(parts[2]); err != nil {
return emptyContext, err
}
flags, err := strconv.ParseUint(parts[3], 10, 8)
if err != nil {
return emptyContext, err
}
context.flags = byte(flags)
return context, nil
}
// TraceID returns the trace ID of this span context
func (c SpanContext) TraceID() TraceID {
return c.traceID
}
// SpanID returns the span ID of this span context
func (c SpanContext) SpanID() SpanID {
return c.spanID
}
// ParentID returns the parent span ID of this span context
func (c SpanContext) ParentID() SpanID {
return c.parentID
}
// NewSpanContext creates a new instance of SpanContext
func NewSpanContext(traceID TraceID, spanID, parentID SpanID, sampled bool, baggage map[string]string) SpanContext {
flags := byte(0)
if sampled {
flags = flagSampled
}
return SpanContext{
traceID: traceID,
spanID: spanID,
parentID: parentID,
flags: flags,
baggage: baggage}
}
// CopyFrom copies data from ctx into this context, including span identity and baggage.
// TODO This is only used by interop.go. Remove once TChannel Go supports OpenTracing.
func (c *SpanContext) CopyFrom(ctx *SpanContext) {
c.traceID = ctx.traceID
c.spanID = ctx.spanID
c.parentID = ctx.parentID
c.flags = ctx.flags
if l := len(ctx.baggage); l > 0 {
c.baggage = make(map[string]string, l)
for k, v := range ctx.baggage {
c.baggage[k] = v
}
} else {
c.baggage = nil
}
}
// WithBaggageItem creates a new context with an extra baggage item.
func (c SpanContext) WithBaggageItem(key, value string) SpanContext {
var newBaggage map[string]string
if c.baggage == nil {
newBaggage = map[string]string{key: value}
} else {
newBaggage = make(map[string]string, len(c.baggage)+1)
for k, v := range c.baggage {
newBaggage[k] = v
}
newBaggage[key] = value
}
// Use positional parameters so the compiler will help catch new fields.
return SpanContext{c.traceID, c.spanID, c.parentID, c.flags, newBaggage, ""}
}
// isDebugIDContainerOnly returns true when the instance of the context is only
// used to return the debug/correlation ID from extract() method. This happens
// in the situation when "jaeger-debug-id" header is passed in the carrier to
// the extract() method, but the request otherwise has no span context in it.
// Previously this would've returned opentracing.ErrSpanContextNotFound from the
// extract method, but now it returns a dummy context with only debugID filled in.
//
// See JaegerDebugHeader in constants.go
// See textMapPropagator#Extract
func (c *SpanContext) isDebugIDContainerOnly() bool {
return !c.traceID.IsValid() && c.debugID != ""
}
// ------- TraceID -------
func (t TraceID) String() string {
if t.High == 0 {
return fmt.Sprintf("%x", t.Low)
}
return fmt.Sprintf("%x%016x", t.High, t.Low)
}
// TraceIDFromString creates a TraceID from a hexadecimal string
func TraceIDFromString(s string) (TraceID, error) {
var hi, lo uint64
var err error
if len(s) > 32 {
return TraceID{}, fmt.Errorf("TraceID cannot be longer than 32 hex characters: %s", s)
} else if len(s) > 16 {
hiLen := len(s) - 16
if hi, err = strconv.ParseUint(s[0:hiLen], 16, 64); err != nil {
return TraceID{}, err
}
if lo, err = strconv.ParseUint(s[hiLen:], 16, 64); err != nil {
return TraceID{}, err
}
} else {
if lo, err = strconv.ParseUint(s, 16, 64); err != nil {
return TraceID{}, err
}
}
return TraceID{High: hi, Low: lo}, nil
}
// IsValid checks if the trace ID is valid, i.e. not zero.
func (t TraceID) IsValid() bool {
return t.High != 0 || t.Low != 0
}
// ------- SpanID -------
func (s SpanID) String() string {
return fmt.Sprintf("%x", uint64(s))
}
// SpanIDFromString creates a SpanID from a hexadecimal string
func SpanIDFromString(s string) (SpanID, error) {
if len(s) > 16 {
return SpanID(0), fmt.Errorf("SpanID cannot be longer than 16 hex characters: %s", s)
}
id, err := strconv.ParseUint(s, 16, 64)
if err != nil {
return SpanID(0), err
}
return SpanID(id), nil
}

View File

@ -0,0 +1,110 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestContextFromString(t *testing.T) {
var err error
_, err = ContextFromString("")
assert.Error(t, err)
_, err = ContextFromString("abcd")
assert.Error(t, err)
_, err = ContextFromString("x:1:1:1")
assert.Error(t, err)
_, err = ContextFromString("1:x:1:1")
assert.Error(t, err)
_, err = ContextFromString("1:1:x:1")
assert.Error(t, err)
_, err = ContextFromString("1:1:1:x")
assert.Error(t, err)
_, err = ContextFromString("1:1:1:x")
assert.Error(t, err)
_, err = ContextFromString("01234567890123456789012345678901234:1:1:1")
assert.Error(t, err)
_, err = ContextFromString("01234567890123456789012345678901:1:1:1")
assert.NoError(t, err)
_, err = ContextFromString("01234_67890123456789012345678901:1:1:1")
assert.Error(t, err)
_, err = ContextFromString("0123456789012345678901_345678901:1:1:1")
assert.Error(t, err)
_, err = ContextFromString("1:0123456789012345:1:1")
assert.NoError(t, err)
_, err = ContextFromString("1:01234567890123456:1:1")
assert.Error(t, err)
ctx, err := ContextFromString("10000000000000001:1:1:1")
assert.NoError(t, err)
assert.EqualValues(t, TraceID{High: 1, Low: 1}, ctx.traceID)
ctx, err = ContextFromString("1:1:1:1")
assert.NoError(t, err)
assert.EqualValues(t, TraceID{Low: 1}, ctx.traceID)
assert.EqualValues(t, 1, ctx.spanID)
assert.EqualValues(t, 1, ctx.parentID)
assert.EqualValues(t, 1, ctx.flags)
ctx = NewSpanContext(TraceID{Low: 1}, 1, 1, true, nil)
assert.EqualValues(t, TraceID{Low: 1}, ctx.traceID)
assert.EqualValues(t, 1, ctx.spanID)
assert.EqualValues(t, 1, ctx.parentID)
assert.EqualValues(t, 1, ctx.flags)
assert.Equal(t, "ff", SpanID(255).String())
assert.Equal(t, "ff", TraceID{Low: 255}.String())
assert.Equal(t, "ff00000000000000ff", TraceID{High: 255, Low: 255}.String())
ctx = NewSpanContext(TraceID{High: 255, Low: 255}, SpanID(1), SpanID(1), false, nil)
assert.Equal(t, "ff00000000000000ff:1:1:0", ctx.String())
}
func TestSpanContext_WithBaggageItem(t *testing.T) {
var ctx SpanContext
ctx = ctx.WithBaggageItem("some-KEY", "Some-Value")
assert.Equal(t, map[string]string{"some-KEY": "Some-Value"}, ctx.baggage)
ctx = ctx.WithBaggageItem("some-KEY", "Some-Other-Value")
assert.Equal(t, map[string]string{"some-KEY": "Some-Other-Value"}, ctx.baggage)
}
func TestSpanContext_SampledDebug(t *testing.T) {
ctx, err := ContextFromString("1:1:1:1")
require.NoError(t, err)
assert.True(t, ctx.IsSampled())
assert.False(t, ctx.IsDebug())
ctx, err = ContextFromString("1:1:1:3")
require.NoError(t, err)
assert.True(t, ctx.IsSampled())
assert.True(t, ctx.IsDebug())
ctx, err = ContextFromString("1:1:1:0")
require.NoError(t, err)
assert.False(t, ctx.IsSampled())
assert.False(t, ctx.IsDebug())
}
func TestSpanContext_CopyFrom(t *testing.T) {
ctx, err := ContextFromString("1:1:1:1")
require.NoError(t, err)
ctx2 := SpanContext{}
ctx2.CopyFrom(&ctx)
assert.Equal(t, ctx, ctx2)
// with baggage
ctx = ctx.WithBaggageItem("x", "y")
ctx2 = SpanContext{}
ctx2.CopyFrom(&ctx)
assert.Equal(t, ctx, ctx2)
assert.Equal(t, "y", ctx2.baggage["x"])
}

View File

@ -0,0 +1,56 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
opentracing "github.com/opentracing/opentracing-go"
)
// ContribObserver can be registered with the Tracer to receive notifications
// about new Spans. Modelled after github.com/opentracing-contrib/go-observer.
type ContribObserver interface {
// Create and return a span observer. Called when a span starts.
// If the Observer is not interested in the given span, it must return (nil, false).
// E.g :
// func StartSpan(opName string, opts ...opentracing.StartSpanOption) {
// var sp opentracing.Span
// sso := opentracing.StartSpanOptions{}
// if spanObserver, ok := Observer.OnStartSpan(span, opName, sso); ok {
// // we have a valid SpanObserver
// }
// ...
// }
OnStartSpan(sp opentracing.Span, operationName string, options opentracing.StartSpanOptions) (ContribSpanObserver, bool)
}
// ContribSpanObserver is created by the Observer and receives notifications
// about other Span events. This interface is meant to match
// github.com/opentracing-contrib/go-observer, via duck typing, without
// directly importing the go-observer package.
type ContribSpanObserver interface {
OnSetOperationName(operationName string)
OnSetTag(key string, value interface{})
OnFinish(options opentracing.FinishOptions)
}
// wrapper observer for the old observers (see observer.go)
type oldObserver struct {
obs Observer
}
func (o *oldObserver) OnStartSpan(sp opentracing.Span, operationName string, options opentracing.StartSpanOptions) (ContribSpanObserver, bool) {
spanObserver := o.obs.OnStartSpan(operationName, options)
return spanObserver, spanObserver != nil
}

View File

@ -0,0 +1,4 @@
FROM scratch
ADD crossdock /
CMD ["/crossdock"]
EXPOSE 8080-8082

View File

@ -0,0 +1,78 @@
# Crossdock-based Integration Test Suite
This package implements integration test suite for testing
interoperability between different Jaeger client libraries.
## Actors
There are five actors participating in any given test case,
the crossdock driver itself, a Client, and three Servers, S1-S3.
### Driver
The crossdock driver reads axis and test definitions from the YAML file,
generates permutations based on values listed for each axis, and
makes an HTTP request to the Client, passing it the selected value
for each axis.
### Client
Client runs as part of the `jaeger-client/go` image and orchestrates
the actual test case with the servers S1-S3. The incoming request
from the driver is expected to have parameters defined in
[client/constants.go](client/constants.go), which specify
1. The type of test to execute (only `trace` is currently supported)
1. Whether the trace should be sampled or not
1. For each of the servers S1-S3:
* the name of the server (same as docker image name, same as host name)
* the transport to send request to that server (http or TChannel)
* the type of client to use (e.g. in Python, `urllib2` vs. `requests`)
The Client translates the parameters into a "call tree" instruction set,
and calls S1, which in turn calls S2 with the sub-set of instructions,
and so on. Upon receiving the response from S1, the Client validates the
response against the conditions of the test, and returns a summary result
to the crossdock driver, indicating a success of a failure of the test.
For the `trace` test type, the success conditions are that at all levels
the observed tracing spans have the same trace ID, the same sampling flag
equal to the input `sampled` parameter, and the same value of a baggage
item. The baggage item value is randomly selected by the client at the
start of each test.
### Servers
Servers represent examples of business services with Jaeger tracing enabled.
Servers must be implemented for each supported language, and potentially
multiple times for a given language depending on the framework used to build
the service, such as Flask vs. Tornado in Python. Each implementation of the
server may act as any of the S1-S3 servers in the test. Each server must
implement the `TracedService` interface from
[thrift/tracetest.thrift](thrift/tracetest.thrift):
service TracedService {
TraceResponse startTrace(1: StartTraceRequest request)
TraceResponse joinTrace(1: JoinTraceRequest request)
}
* In `startTrace` the server is supposed to ignore any trace it may have
received via inbound request and start a brand new trace, with the
sampling flag set appropriately, using `sampling.priority` tag,
see [Go server implementation](server/trace.go) for example.
* In `joinTrace` the server is supposed to respect the trace in the
inbound request and propagate it to the outbound downstream request.
The response from the server contains the information about the current
tracing span it has observed (or started), including trace ID, sampling
flag, and the value of a baggage item. For S1 and S2 the response also
includes the response of the downstream server.
## Running the tests
The intended setup is that every commit to master branch of each of the client
libraries results in a build of a new docker image (or images, e.g. in Python).
When a new pull request is tested against one of the libraries, it will build
a new image from the modified version of the library, and use the existing
images for the other languages. The `docker-compose.yaml` file refers to those
images by name.

View File

@ -0,0 +1,109 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"fmt"
"net"
"net/http"
"sync"
"github.com/crossdock/crossdock-go"
"github.com/uber/jaeger-client-go/crossdock/common"
)
// Client is a controller for the tests
type Client struct {
ClientHostPort string
ServerPortHTTP string
ServerPortTChannel string
listener net.Listener
hostMapper func(service string) string
}
// Start begins a blocking Crossdock client
func (c *Client) Start() error {
if err := c.Listen(); err != nil {
return err
}
return c.Serve()
}
// AsyncStart begins a Crossdock client in the background,
// but does not return until it started serving.
func (c *Client) AsyncStart() error {
if err := c.Listen(); err != nil {
return err
}
var started sync.WaitGroup
started.Add(1)
go func() {
started.Done()
c.Serve()
}()
started.Wait()
return nil
}
// Listen initializes the server
func (c *Client) Listen() error {
c.setDefaultPort(&c.ClientHostPort, ":"+common.DefaultClientPortHTTP)
c.setDefaultPort(&c.ServerPortHTTP, common.DefaultServerPortHTTP)
c.setDefaultPort(&c.ServerPortTChannel, common.DefaultServerPortTChannel)
behaviors := crossdock.Behaviors{
behaviorTrace: c.trace,
}
http.Handle("/", crossdock.Handler(behaviors, true))
listener, err := net.Listen("tcp", c.ClientHostPort)
if err != nil {
return err
}
c.listener = listener
c.ClientHostPort = listener.Addr().String()
return nil
}
// Serve starts service crossdock traffic. This is a blocking call.
func (c *Client) Serve() error {
return http.Serve(c.listener, nil)
}
// Close stops the client
func (c *Client) Close() error {
return c.listener.Close()
}
// URL returns a URL that the client can be accessed on
func (c *Client) URL() string {
return fmt.Sprintf("http://%s/", c.ClientHostPort)
}
func (c *Client) setDefaultPort(port *string, defaultPort string) {
if *port == "" {
*port = defaultPort
}
}
func (c *Client) mapServiceToHost(service string) string {
mapper := c.hostMapper
if mapper == nil {
return service
}
return mapper(service)
}

View File

@ -0,0 +1,109 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"net/url"
"testing"
"github.com/crossdock/crossdock-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/crossdock/common"
"github.com/uber/jaeger-client-go/crossdock/log"
"github.com/uber/jaeger-client-go/crossdock/server"
jlog "github.com/uber/jaeger-client-go/log"
)
func TestCrossdock(t *testing.T) {
log.Enabled = false // enable when debugging tests
log.Printf("Starting crossdock test")
var reporter jaeger.Reporter
if log.Enabled {
reporter = jaeger.NewLoggingReporter(jlog.StdLogger)
} else {
reporter = jaeger.NewNullReporter()
}
tracer, tCloser := jaeger.NewTracer(
"crossdock",
jaeger.NewConstSampler(false),
reporter)
defer tCloser.Close()
s := &server.Server{
HostPortHTTP: "127.0.0.1:0",
HostPortTChannel: "127.0.0.1:0",
Tracer: tracer,
}
err := s.Start()
require.NoError(t, err)
defer s.Close()
c := &Client{
ClientHostPort: "127.0.0.1:0",
ServerPortHTTP: s.GetPortHTTP(),
ServerPortTChannel: s.GetPortTChannel(),
hostMapper: func(server string) string { return "localhost" },
}
err = c.AsyncStart()
require.NoError(t, err)
defer c.Close()
crossdock.Wait(t, s.URL(), 10)
crossdock.Wait(t, c.URL(), 10)
behaviors := []struct {
name string
axes map[string][]string
}{
{
name: behaviorTrace,
axes: map[string][]string{
server1NameParam: {common.DefaultServiceName},
sampledParam: {"true", "false"},
server2NameParam: {common.DefaultServiceName},
server2TransportParam: {transportHTTP, transportTChannel, transportDummy},
server3NameParam: {common.DefaultServiceName},
server3TransportParam: {transportHTTP, transportTChannel},
},
},
}
for _, bb := range behaviors {
for _, entry := range crossdock.Combinations(bb.axes) {
entryArgs := url.Values{}
for k, v := range entry {
entryArgs.Set(k, v)
}
// test via real HTTP call
crossdock.Call(t, c.URL(), bb.name, entryArgs)
}
}
}
func TestHostMapper(t *testing.T) {
c := &Client{
ClientHostPort: "127.0.0.1:0",
ServerPortHTTP: "8080",
ServerPortTChannel: "8081",
}
assert.Equal(t, "go", c.mapServiceToHost("go"))
c.hostMapper = func(server string) string { return "localhost" }
assert.Equal(t, "localhost", c.mapServiceToHost("go"))
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
// Different parameter keys and values used by the system
const (
// S1 instructions
sampledParam = "sampled"
server1NameParam = "s1name"
// S1->S2 instructions
server2NameParam = "s2name"
server2TransportParam = "s2transport"
// S2->S3 instructions
server3NameParam = "s3name"
server3TransportParam = "s3transport"
transportHTTP = "http"
transportTChannel = "tchannel"
transportDummy = "dummy"
behaviorTrace = "trace"
// RoleS1 is the name of the role for server S1
RoleS1 = "S1"
// RoleS2 is the name of the role for server S2
RoleS2 = "S2"
// RoleS3 is the name of the role for server S3
RoleS3 = "S3"
)

View File

@ -0,0 +1,167 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"fmt"
"time"
"github.com/crossdock/crossdock-go"
"golang.org/x/net/context"
"github.com/uber/jaeger-client-go/crossdock/common"
"github.com/uber/jaeger-client-go/crossdock/log"
"github.com/uber/jaeger-client-go/crossdock/thrift/tracetest"
"github.com/uber/jaeger-client-go/utils"
)
func (c *Client) trace(t crossdock.T) {
sampled := str2bool(t.Param(sampledParam))
baggage := randomBaggage()
level1 := tracetest.NewStartTraceRequest()
level1.ServerRole = RoleS1
level1.Sampled = sampled
level1.Baggage = baggage
server1 := t.Param(server1NameParam)
level2 := tracetest.NewDownstream()
level2.ServiceName = t.Param(server2NameParam)
level2.ServerRole = RoleS2
level2.Host = c.mapServiceToHost(level2.ServiceName)
level2.Port = c.transport2port(t.Param(server2TransportParam))
level2.Transport = transport2transport(t.Param(server2TransportParam))
level1.Downstream = level2
level3 := tracetest.NewDownstream()
level3.ServiceName = t.Param(server3NameParam)
level3.ServerRole = RoleS3
level3.Host = c.mapServiceToHost(level3.ServiceName)
level3.Port = c.transport2port(t.Param(server3TransportParam))
level3.Transport = transport2transport(t.Param(server3TransportParam))
level2.Downstream = level3
server1host := c.mapServiceToHost(server1)
url := fmt.Sprintf("http://%s:%s/start_trace", server1host, c.ServerPortHTTP)
resp, err := common.PostJSON(context.Background(), url, level1)
if err != nil {
t.Errorf(err.Error())
return
}
for r := resp; r != nil; r = r.Downstream {
if r.NotImplementedError != "" {
t.Skipf(r.NotImplementedError)
log.Printf("SKIP: %s", r.NotImplementedError)
return
}
}
traceID := resp.Span.TraceId
if traceID == "" {
t.Errorf("Trace ID is empty in S1(%s)", server1)
return
}
success := validateTrace(t, level1.Downstream, resp, server1, 1, traceID, sampled, baggage)
if success {
t.Successf("trace checks out")
log.Printf("PASS")
}
}
func validateTrace(
t crossdock.T,
target *tracetest.Downstream,
resp *tracetest.TraceResponse,
service string,
level int,
traceID string,
sampled bool,
baggage string) bool {
success := true
if traceID != resp.Span.TraceId {
t.Errorf("Trace ID mismatch in S%d(%s): expected %s, received %s",
level, service, traceID, resp.Span.TraceId)
success = false
}
if baggage != resp.Span.Baggage {
t.Errorf("Baggage mismatch in S%d(%s): expected %s, received %s",
level, service, baggage, resp.Span.Baggage)
success = false
}
if sampled != resp.Span.Sampled {
t.Errorf("Sampled mismatch in S%d(%s): expected %t, received %t",
level, service, sampled, resp.Span.Sampled)
success = false
}
if target != nil {
if resp.Downstream == nil {
t.Errorf("Missing downstream in S%d(%s)", level, service)
success = false
} else {
success = validateTrace(t, target.Downstream, resp.Downstream,
target.Host, level+1, traceID, sampled, baggage) && success
}
} else if resp.Downstream != nil {
t.Errorf("Unexpected downstream in S%d(%s)", level, service)
success = false
}
return success
}
func randomBaggage() string {
r := utils.NewRand(time.Now().UnixNano())
n := uint64(r.Int63())
return fmt.Sprintf("%x", n)
}
func str2bool(v string) bool {
switch v {
case "true":
return true
case "false":
return false
default:
panic(v + " is not a Boolean")
}
}
func (c *Client) transport2port(v string) string {
switch v {
case transportHTTP:
return c.ServerPortHTTP
case transportTChannel:
return c.ServerPortTChannel
case transportDummy:
return "9999"
default:
panic("Unknown protocol " + v)
}
}
func transport2transport(v string) tracetest.Transport {
switch v {
case transportHTTP:
return tracetest.Transport_HTTP
case transportTChannel:
return tracetest.Transport_TCHANNEL
case transportDummy:
return tracetest.Transport_DUMMY
default:
panic("Unknown protocol " + v)
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
const (
// DefaultClientPortHTTP is the port where the client (controller) runs
DefaultClientPortHTTP = "8080"
// DefaultServerPortHTTP is the port where HTTP server runs
DefaultServerPortHTTP = "8081"
// DefaultServerPortTChannel is the port where TChannel server runs
DefaultServerPortTChannel = "8082"
// DefaultServiceName is the service name used by TChannel server
DefaultServiceName = "go"
// DefaultTracerServiceName is the service name used by the tracer
DefaultTracerServiceName = "crossdock-go"
)

View File

@ -0,0 +1,73 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"bytes"
"encoding/json"
"net/http"
"github.com/uber/jaeger-client-go/crossdock/thrift/tracetest"
"github.com/uber/jaeger-client-go/utils"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"golang.org/x/net/context"
)
// PostJSON sends a POST request to `url` with body containing JSON-serialized `req`.
// It injects tracing span into the headers (if found in the context).
// It returns parsed TraceResponse, or error.
func PostJSON(ctx context.Context, url string, req interface{}) (*tracetest.TraceResponse, error) {
data, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
span, err := injectSpan(ctx, httpReq)
if span != nil {
defer span.Finish()
}
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
var result tracetest.TraceResponse
err = utils.ReadJSON(resp, &result)
return &result, err
}
func injectSpan(ctx context.Context, req *http.Request) (opentracing.Span, error) {
span := opentracing.SpanFromContext(ctx)
if span == nil {
return nil, nil
}
span = span.Tracer().StartSpan("post", opentracing.ChildOf(span.Context()))
ext.SpanKindRPCClient.Set(span)
c := opentracing.HTTPHeadersCarrier(req.Header)
err := span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, c)
return span, err
}

View File

@ -0,0 +1,71 @@
version: '2'
services:
crossdock:
image: crossdock/crossdock
links:
- test_driver
- go
- java
- python
environment:
- WAIT_FOR=test_driver,go,java,python
- WAIT_FOR_TIMEOUT=60s
- CALL_TIMEOUT=60s
- AXIS_CLIENT=go
- AXIS_S1NAME=go,java,python
- AXIS_SAMPLED=true,false
- AXIS_S2NAME=go,java,python
- AXIS_S2TRANSPORT=http,tchannel
- AXIS_S3NAME=go,java,python
- AXIS_S3TRANSPORT=http,tchannel
- BEHAVIOR_TRACE=client,s1name,sampled,s2name,s2transport,s3name,s3transport
- AXIS_TESTDRIVER=test_driver
- AXIS_SERVICES=go
- BEHAVIOR_ENDTOEND=testdriver,services
- REPORT=compact
go:
build: .
ports:
- "8080-8082"
java:
image: jaegertracing/xdock-java
ports:
- "8080-8082"
python:
image: jaegertracing/xdock-py
ports:
- "8080:8082"
cassandra:
image: "cassandra:3.9"
test_driver:
image: jaegertracing/test-driver
links:
- cassandra
depends_on:
- cassandra
ports:
- "8080"
# node:
# image: yarpc/yarpc-node
# ports:
# - "8080-8082"
#
# python-sync:
# image: yarpc/yarpc-python
# ports:
# - 8080
# environment:
# - SYNC=1

View File

@ -0,0 +1,137 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package endtoend
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-client-go/crossdock/common"
"github.com/uber/jaeger-client-go/crossdock/log"
)
var (
defaultSamplerType = jaeger.SamplerTypeRemote
endToEndConfig = config.Configuration{
Disabled: false,
Sampler: &config.SamplerConfig{
Type: defaultSamplerType,
Param: 1.0,
SamplingServerURL: "http://test_driver:5778/sampling",
SamplingRefreshInterval: 5 * time.Second,
},
Reporter: &config.ReporterConfig{
BufferFlushInterval: time.Second,
LocalAgentHostPort: "test_driver:5775",
},
}
)
/*Handler handles creating traces from a http request.
*
* json: {
* "type": "remote",
* "operation": "operationName",
* "count": 2,
* "tags": {
* "key": "value"
* }
* }
*
* Given the above json payload, the handler will use a tracer with the RemotelyControlledSampler
* to create 2 traces for "operationName" operation with the tags: {"key":"value"}. These traces
* are reported to the agent with the hostname "test_driver".
*/
type Handler struct {
sync.RWMutex
tracers map[string]opentracing.Tracer
}
type traceRequest struct {
Type string `json:"type"`
Operation string `json:"operation"`
Tags map[string]string `json:"tags"`
Count int `json:"count"`
}
// NewHandler returns a Handler.
func NewHandler() *Handler {
return &Handler{
tracers: make(map[string]opentracing.Tracer),
}
}
// init initializes the handler with a tracer
func (h *Handler) init(cfg config.Configuration) error {
tracer, _, err := cfg.New(common.DefaultTracerServiceName)
if err != nil {
return err
}
h.tracers[cfg.Sampler.Type] = tracer
return nil
}
func (h *Handler) getTracer(samplerType string) opentracing.Tracer {
if samplerType == "" {
samplerType = defaultSamplerType
}
h.Lock()
defer h.Unlock()
tracer, ok := h.tracers[samplerType]
if !ok {
endToEndConfig.Sampler.Type = samplerType
if err := h.init(endToEndConfig); err != nil {
log.Printf("Failed to create tracer: %s", err.Error())
return nil
}
tracer, _ = h.tracers[samplerType]
}
return tracer
}
// GenerateTraces creates traces given the parameters in the request.
func (h *Handler) GenerateTraces(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var req traceRequest
if err := decoder.Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("JSON payload is invalid: %s", err.Error()), http.StatusBadRequest)
return
}
tracer := h.getTracer(req.Type)
if tracer == nil {
http.Error(w, "Tracer is not initialized", http.StatusInternalServerError)
return
}
generateTraces(tracer, &req)
}
func generateTraces(tracer opentracing.Tracer, r *traceRequest) {
for i := 0; i < r.Count; i++ {
span := tracer.StartSpan(r.Operation)
for k, v := range r.Tags {
span.SetTag(k, v)
}
span.Finish()
}
}

View File

@ -0,0 +1,154 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package endtoend
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-client-go/log"
)
var (
testOperation = "testOperation"
testService = "testService"
testConfig = config.Configuration{
Disabled: false,
Sampler: &config.SamplerConfig{
Type: jaeger.SamplerTypeRemote,
Param: 1.0,
SamplingServerURL: "http://localhost:5778/sampling",
},
Reporter: &config.ReporterConfig{
BufferFlushInterval: time.Second,
LocalAgentHostPort: "localhost:5775",
},
}
badConfig = config.Configuration{
Disabled: false,
Sampler: &config.SamplerConfig{
Type: "INVALID_TYPE",
},
}
testTraceRequest = traceRequest{
Type: jaeger.SamplerTypeConst,
Operation: testOperation,
Tags: map[string]string{
"key": "value",
},
Count: 2,
}
testInvalidJSON = `bad_json`
testTraceJSONRequest = `
{
"type": "const",
"operation": "testOperation",
"tags": {
"key": "value"
},
"count": 2
}
`
testInvalidTypeJSONRequest = `
{
"type": "INVALID_TYPE",
"operation": "testOperation",
"tags": {
"key": "value"
},
"count": 2
}
`
)
func newInMemoryTracer() (opentracing.Tracer, *jaeger.InMemoryReporter) {
inMemoryReporter := jaeger.NewInMemoryReporter()
tracer, _ := jaeger.NewTracer(
testService,
jaeger.NewConstSampler(true),
inMemoryReporter,
jaeger.TracerOptions.Metrics(jaeger.NewNullMetrics()),
jaeger.TracerOptions.Logger(log.NullLogger))
return tracer, inMemoryReporter
}
func TestInit(t *testing.T) {
handler := NewHandler()
err := handler.init(testConfig)
assert.NoError(t, err)
}
func TestInitBadConfig(t *testing.T) {
handler := NewHandler()
err := handler.init(badConfig)
assert.Error(t, err)
}
func TestGetTracer(t *testing.T) {
iTracer, _ := newInMemoryTracer()
handler := &Handler{tracers: map[string]opentracing.Tracer{jaeger.SamplerTypeConst: iTracer}}
tracer := handler.getTracer(jaeger.SamplerTypeConst)
assert.Equal(t, iTracer, tracer)
}
func TestGetTracerError(t *testing.T) {
handler := NewHandler()
tracer := handler.getTracer("INVALID_TYPE")
assert.Nil(t, tracer)
}
func TestGenerateTraces(t *testing.T) {
tracer, _ := newInMemoryTracer()
tests := []struct {
expectedCode int
json string
handler *Handler
}{
{http.StatusOK, testTraceJSONRequest, &Handler{tracers: map[string]opentracing.Tracer{jaeger.SamplerTypeConst: tracer}}},
{http.StatusBadRequest, testInvalidJSON, NewHandler()},
{http.StatusInternalServerError, testInvalidTypeJSONRequest, NewHandler()}, // Tracer failed to initialize
}
for _, test := range tests {
req, err := http.NewRequest("POST", "/create_spans", bytes.NewBuffer([]byte(test.json)))
assert.NoError(t, err, "Failed to initialize request")
recorder := httptest.NewRecorder()
handlerFunc := http.HandlerFunc(test.handler.GenerateTraces)
handlerFunc.ServeHTTP(recorder, req)
assert.Equal(t, test.expectedCode, recorder.Code)
}
}
func TestGenerateTracesInternal(t *testing.T) {
tracer, reporter := newInMemoryTracer()
generateTraces(tracer, &testTraceRequest)
assert.Equal(t, 2, reporter.SpansSubmitted())
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import (
real_log "log"
)
// Enabled controls logging from crossdock tests. It is enabled in main.go, but off in unit tests.
var Enabled bool
// Printf delegates to log.Printf if Enabled == true
func Printf(msg string, args ...interface{}) {
if Enabled {
real_log.Printf(msg, args)
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"io"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/crossdock/client"
"github.com/uber/jaeger-client-go/crossdock/common"
"github.com/uber/jaeger-client-go/crossdock/log"
"github.com/uber/jaeger-client-go/crossdock/server"
jlog "github.com/uber/jaeger-client-go/log"
)
func main() {
log.Enabled = true
tracer, tCloser := initTracer()
defer tCloser.Close()
s := &server.Server{Tracer: tracer}
if err := s.Start(); err != nil {
panic(err.Error())
} else {
defer s.Close()
}
client := &client.Client{}
if err := client.Start(); err != nil {
panic(err.Error())
}
}
func initTracer() (opentracing.Tracer, io.Closer) {
t, c := jaeger.NewTracer(
common.DefaultTracerServiceName,
jaeger.NewConstSampler(false),
jaeger.NewLoggingReporter(jlog.StdLogger))
return t, c
}

View File

@ -0,0 +1,25 @@
XDOCK_YAML=crossdock/docker-compose.yml
.PHONY: crossdock-linux-bin
crossdock-linux-bin:
CGO_ENABLED=0 GOOS=linux time go build -a -installsuffix cgo -o crossdock/crossdock ./crossdock
.PHONY: crossdock
crossdock: crossdock-linux-bin
docker-compose -f $(XDOCK_YAML) kill go
docker-compose -f $(XDOCK_YAML) rm -f go
docker-compose -f $(XDOCK_YAML) build go
docker-compose -f $(XDOCK_YAML) run crossdock 2>&1 | tee run-crossdock.log
grep 'Tests passed!' run-crossdock.log
.PHONY: crossdock-fresh
crossdock-fresh: crossdock-linux-bin
docker-compose -f $(XDOCK_YAML) kill
docker-compose -f $(XDOCK_YAML) rm --force
docker-compose -f $(XDOCK_YAML) pull
docker-compose -f $(XDOCK_YAML) build
docker-compose -f $(XDOCK_YAML) run crossdock
.PHONE: crossdock-logs
crossdock-logs:
docker-compose -f $(XDOCK_YAML) logs

View File

@ -0,0 +1,26 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import "errors"
// BaggageKey is the key used to pass baggage item
const BaggageKey = "crossdock-baggage-key"
var (
errNoSpanObserved = errors.New("no span found in Context")
errUnrecognizedProtocol = errors.New("unrecognized protocol for downstream call")
errCannotStartInTChannel = errors.New("cannot start new trace in tchannel server")
)

View File

@ -0,0 +1,164 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"sync"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/uber/tchannel-go"
"golang.org/x/net/context"
"github.com/uber/jaeger-client-go/crossdock/common"
"github.com/uber/jaeger-client-go/crossdock/endtoend"
"github.com/uber/jaeger-client-go/crossdock/log"
"github.com/uber/jaeger-client-go/crossdock/thrift/tracetest"
)
// Server implements S1-S3 servers
type Server struct {
HostPortHTTP string
HostPortTChannel string
Tracer opentracing.Tracer
listener net.Listener
channel *tchannel.Channel
eHandler *endtoend.Handler
}
// Start starts the test server called by the Client and other upstream servers.
func (s *Server) Start() error {
if s.HostPortHTTP == "" {
s.HostPortHTTP = ":" + common.DefaultServerPortHTTP
}
if s.HostPortTChannel == "" {
s.HostPortTChannel = ":" + common.DefaultServerPortTChannel
}
if err := s.startTChannelServer(s.Tracer); err != nil {
return err
}
s.eHandler = endtoend.NewHandler()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { return }) // health check
mux.HandleFunc("/start_trace", func(w http.ResponseWriter, r *http.Request) {
s.handleJSON(w, r, func() interface{} {
return tracetest.NewStartTraceRequest()
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return s.doStartTrace(req.(*tracetest.StartTraceRequest))
})
})
mux.HandleFunc("/join_trace", func(w http.ResponseWriter, r *http.Request) {
s.handleJSON(w, r, func() interface{} {
return tracetest.NewJoinTraceRequest()
}, func(ctx context.Context, req interface{}) (interface{}, error) {
return s.doJoinTrace(ctx, req.(*tracetest.JoinTraceRequest))
})
})
mux.HandleFunc("/create_traces", s.eHandler.GenerateTraces)
listener, err := net.Listen("tcp", s.HostPortHTTP)
if err != nil {
return err
}
s.listener = listener
s.HostPortHTTP = listener.Addr().String()
var started sync.WaitGroup
started.Add(1)
go func() {
started.Done()
http.Serve(listener, mux)
}()
started.Wait()
log.Printf("Started http server at %s\n", s.HostPortHTTP)
return nil
}
// URL returns URL of the HTTP server
func (s *Server) URL() string {
return fmt.Sprintf("http://%s/", s.HostPortHTTP)
}
// Close stops the server
func (s *Server) Close() error {
return s.listener.Close()
}
// GetPortHTTP returns the network port the server listens to.
func (s *Server) GetPortHTTP() string {
hostPort := s.HostPortHTTP
hostPortSplit := strings.Split(hostPort, ":")
port := hostPortSplit[len(hostPortSplit)-1]
return port
}
// GetPortTChannel returns the actual port the server listens to
func (s *Server) GetPortTChannel() string {
hostPortSplit := strings.Split(s.HostPortTChannel, ":")
port := hostPortSplit[len(hostPortSplit)-1]
return port
}
func (s *Server) handleJSON(
w http.ResponseWriter,
r *http.Request,
newReq func() interface{},
handle func(ctx context.Context, req interface{}) (interface{}, error),
) {
spanCtx, err := s.Tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
if err != nil && err != opentracing.ErrSpanContextNotFound {
http.Error(w, fmt.Sprintf("Cannot read request body: %+v", err), http.StatusBadRequest)
return
}
span := s.Tracer.StartSpan("post", ext.RPCServerOption(spanCtx))
ctx := opentracing.ContextWithSpan(context.Background(), span)
defer span.Finish()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Cannot read request body: %+v", err), http.StatusInternalServerError)
return
}
log.Printf("Server request: %s", string(body))
req := newReq()
if err := json.Unmarshal(body, req); err != nil {
http.Error(w, fmt.Sprintf("Cannot parse request JSON: %+v. body=[%s]", err, string(body)), http.StatusBadRequest)
return
}
resp, err := handle(ctx, req)
if err != nil {
log.Printf("Handle error: %s", err.Error())
http.Error(w, fmt.Sprintf("Execution error: %+v", err), http.StatusInternalServerError)
return
}
json, err := json.Marshal(resp)
if err != nil {
http.Error(w, fmt.Sprintf("Cannot marshall response to JSON: %+v", err), http.StatusInternalServerError)
return
}
log.Printf("Server response: %s", string(json))
w.Header().Add("Content-Type", "application/json")
if _, err := w.Write(json); err != nil {
return
}
}

View File

@ -0,0 +1,87 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"testing"
"github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/crossdock/common"
"github.com/uber/jaeger-client-go/crossdock/log"
"github.com/uber/jaeger-client-go/crossdock/thrift/tracetest"
)
func TestServerJSON(t *testing.T) {
tracer, tCloser := jaeger.NewTracer(
"crossdock",
jaeger.NewConstSampler(false),
jaeger.NewNullReporter())
defer tCloser.Close()
s := &Server{HostPortHTTP: "127.0.0.1:0", HostPortTChannel: "127.0.0.1:0", Tracer: tracer}
err := s.Start()
require.NoError(t, err)
defer s.Close()
req := tracetest.NewStartTraceRequest()
req.Sampled = true
req.Baggage = "Zoidberg"
req.Downstream = &tracetest.Downstream{
ServiceName: "go",
Host: "localhost",
Port: s.GetPortHTTP(),
Transport: tracetest.Transport_HTTP,
Downstream: &tracetest.Downstream{
ServiceName: "go",
Host: "localhost",
Port: s.GetPortTChannel(),
Transport: tracetest.Transport_TCHANNEL,
},
}
url := fmt.Sprintf("http://%s/start_trace", s.HostPortHTTP)
result, err := common.PostJSON(context.Background(), url, req)
require.NoError(t, err)
log.Printf("response=%+v", &result)
}
func TestObserveSpan(t *testing.T) {
tracer, tCloser := jaeger.NewTracer(
"crossdock",
jaeger.NewConstSampler(true),
jaeger.NewNullReporter())
defer tCloser.Close()
_, err := observeSpan(context.Background(), tracer)
assert.Error(t, err)
span := tracer.StartSpan("hi")
span.SetBaggageItem(BaggageKey, "xyz")
ctx := opentracing.ContextWithSpan(context.Background(), span)
s, err := observeSpan(ctx, tracer)
assert.NoError(t, err)
assert.True(t, s.Sampled)
traceID := span.Context().(jaeger.SpanContext).TraceID().String()
assert.Equal(t, traceID, s.TraceId)
assert.Equal(t, "xyz", s.Baggage)
}

View File

@ -0,0 +1,88 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"time"
"github.com/opentracing/opentracing-go"
"github.com/uber/tchannel-go"
"github.com/uber/tchannel-go/thrift"
"golang.org/x/net/context"
"github.com/uber/jaeger-client-go/crossdock/log"
"github.com/uber/jaeger-client-go/crossdock/thrift/tracetest"
)
func (s *Server) startTChannelServer(tracer opentracing.Tracer) error {
channelOpts := &tchannel.ChannelOptions{
Tracer: tracer,
}
ch, err := tchannel.NewChannel("go", channelOpts)
if err != nil {
return err
}
server := thrift.NewServer(ch)
s.channel = ch
handler := tracetest.NewTChanTracedServiceServer(s)
server.Register(handler)
if err := ch.ListenAndServe(s.HostPortTChannel); err != nil {
return err
}
s.HostPortTChannel = ch.PeerInfo().HostPort
log.Printf("Started tchannel server at %s\n", s.HostPortTChannel)
return nil
}
// StartTrace implements StartTrace() of TChanTracedService
func (s *Server) StartTrace(ctx thrift.Context, request *tracetest.StartTraceRequest) (*tracetest.TraceResponse, error) {
return nil, errCannotStartInTChannel
}
// JoinTrace implements JoinTrace() of TChanTracedService
func (s *Server) JoinTrace(ctx thrift.Context, request *tracetest.JoinTraceRequest) (*tracetest.TraceResponse, error) {
log.Printf("tchannel server handling JoinTrace")
return s.prepareResponse(ctx, request.ServerRole, request.Downstream)
}
func (s *Server) callDownstreamTChannel(ctx context.Context, target *tracetest.Downstream) (*tracetest.TraceResponse, error) {
req := &tracetest.JoinTraceRequest{
ServerRole: target.ServerRole,
Downstream: target.Downstream,
}
hostPort := fmt.Sprintf("%s:%s", target.Host, target.Port)
log.Printf("calling downstream '%s' over tchannel:%s", target.ServiceName, hostPort)
channelOpts := &tchannel.ChannelOptions{
Tracer: s.Tracer,
}
ch, err := tchannel.NewChannel("tchannel-client", channelOpts)
if err != nil {
return nil, err
}
opts := &thrift.ClientOptions{HostPort: hostPort}
thriftClient := thrift.NewClient(ch, target.ServiceName, opts)
client := tracetest.NewTChanTracedServiceClient(thriftClient)
ctx, cx := context.WithTimeout(ctx, time.Second)
defer cx()
return client.JoinTrace(thrift.Wrap(ctx), req)
}

View File

@ -0,0 +1,101 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"golang.org/x/net/context"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/crossdock/common"
"github.com/uber/jaeger-client-go/crossdock/log"
"github.com/uber/jaeger-client-go/crossdock/thrift/tracetest"
)
func (s *Server) doStartTrace(req *tracetest.StartTraceRequest) (*tracetest.TraceResponse, error) {
span := s.Tracer.StartSpan(req.ServerRole)
if req.Sampled {
ext.SamplingPriority.Set(span, 1)
}
span.SetBaggageItem(BaggageKey, req.Baggage)
defer span.Finish()
ctx := opentracing.ContextWithSpan(context.Background(), span)
return s.prepareResponse(ctx, req.ServerRole, req.Downstream)
}
func (s *Server) doJoinTrace(ctx context.Context, req *tracetest.JoinTraceRequest) (*tracetest.TraceResponse, error) {
return s.prepareResponse(ctx, req.ServerRole, req.Downstream)
}
func (s *Server) prepareResponse(ctx context.Context, role string, reqDwn *tracetest.Downstream) (*tracetest.TraceResponse, error) {
observedSpan, err := observeSpan(ctx, s.Tracer)
if err != nil {
return nil, err
}
resp := tracetest.NewTraceResponse()
resp.Span = observedSpan
if reqDwn != nil {
downstreamResp, err := s.callDownstream(ctx, role, reqDwn)
if err != nil {
return nil, err
}
resp.Downstream = downstreamResp
}
return resp, nil
}
func (s *Server) callDownstream(ctx context.Context, role string, downstream *tracetest.Downstream) (*tracetest.TraceResponse, error) {
switch downstream.Transport {
case tracetest.Transport_HTTP:
return s.callDownstreamHTTP(ctx, downstream)
case tracetest.Transport_TCHANNEL:
return s.callDownstreamTChannel(ctx, downstream)
case tracetest.Transport_DUMMY:
return &tracetest.TraceResponse{NotImplementedError: "DUMMY transport not implemented"}, nil
default:
return nil, errUnrecognizedProtocol
}
}
func (s *Server) callDownstreamHTTP(ctx context.Context, target *tracetest.Downstream) (*tracetest.TraceResponse, error) {
req := &tracetest.JoinTraceRequest{
ServerRole: target.ServerRole,
Downstream: target.Downstream,
}
url := fmt.Sprintf("http://%s:%s/join_trace", target.Host, target.Port)
log.Printf("Calling downstream service '%s' at %s", target.ServiceName, url)
return common.PostJSON(ctx, url, req)
}
func observeSpan(ctx context.Context, tracer opentracing.Tracer) (*tracetest.ObservedSpan, error) {
span := opentracing.SpanFromContext(ctx)
if span == nil {
return nil, errNoSpanObserved
}
sc := span.Context().(jaeger.SpanContext)
observedSpan := tracetest.NewObservedSpan()
observedSpan.TraceId = sc.TraceID().String()
observedSpan.Sampled = sc.IsSampled()
observedSpan.Baggage = span.BaggageItem(BaggageKey)
return observedSpan, nil
}

View File

@ -0,0 +1,18 @@
// Autogenerated by Thrift Compiler (0.9.3)
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
package tracetest
import (
"bytes"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = bytes.Equal
func init() {
}

View File

@ -0,0 +1,137 @@
// @generated Code generated by thrift-gen. Do not modify.
// Package tracetest is generated code used to make or handle TChannel calls using Thrift.
package tracetest
import (
"fmt"
athrift "github.com/apache/thrift/lib/go/thrift"
"github.com/uber/tchannel-go/thrift"
)
// Interfaces for the service and client for the services defined in the IDL.
// TChanTracedService is the interface that defines the server handler and client interface.
type TChanTracedService interface {
JoinTrace(ctx thrift.Context, request *JoinTraceRequest) (*TraceResponse, error)
StartTrace(ctx thrift.Context, request *StartTraceRequest) (*TraceResponse, error)
}
// Implementation of a client and service handler.
type tchanTracedServiceClient struct {
thriftService string
client thrift.TChanClient
}
func NewTChanTracedServiceInheritedClient(thriftService string, client thrift.TChanClient) *tchanTracedServiceClient {
return &tchanTracedServiceClient{
thriftService,
client,
}
}
// NewTChanTracedServiceClient creates a client that can be used to make remote calls.
func NewTChanTracedServiceClient(client thrift.TChanClient) TChanTracedService {
return NewTChanTracedServiceInheritedClient("TracedService", client)
}
func (c *tchanTracedServiceClient) JoinTrace(ctx thrift.Context, request *JoinTraceRequest) (*TraceResponse, error) {
var resp TracedServiceJoinTraceResult
args := TracedServiceJoinTraceArgs{
Request: request,
}
success, err := c.client.Call(ctx, c.thriftService, "joinTrace", &args, &resp)
if err == nil && !success {
}
return resp.GetSuccess(), err
}
func (c *tchanTracedServiceClient) StartTrace(ctx thrift.Context, request *StartTraceRequest) (*TraceResponse, error) {
var resp TracedServiceStartTraceResult
args := TracedServiceStartTraceArgs{
Request: request,
}
success, err := c.client.Call(ctx, c.thriftService, "startTrace", &args, &resp)
if err == nil && !success {
}
return resp.GetSuccess(), err
}
type tchanTracedServiceServer struct {
handler TChanTracedService
}
// NewTChanTracedServiceServer wraps a handler for TChanTracedService so it can be
// registered with a thrift.Server.
func NewTChanTracedServiceServer(handler TChanTracedService) thrift.TChanServer {
return &tchanTracedServiceServer{
handler,
}
}
func (s *tchanTracedServiceServer) Service() string {
return "TracedService"
}
func (s *tchanTracedServiceServer) Methods() []string {
return []string{
"joinTrace",
"startTrace",
}
}
func (s *tchanTracedServiceServer) Handle(ctx thrift.Context, methodName string, protocol athrift.TProtocol) (bool, athrift.TStruct, error) {
switch methodName {
case "joinTrace":
return s.handleJoinTrace(ctx, protocol)
case "startTrace":
return s.handleStartTrace(ctx, protocol)
default:
return false, nil, fmt.Errorf("method %v not found in service %v", methodName, s.Service())
}
}
func (s *tchanTracedServiceServer) handleJoinTrace(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) {
var req TracedServiceJoinTraceArgs
var res TracedServiceJoinTraceResult
if err := req.Read(protocol); err != nil {
return false, nil, err
}
r, err :=
s.handler.JoinTrace(ctx, req.Request)
if err != nil {
return false, nil, err
} else {
res.Success = r
}
return err == nil, &res, nil
}
func (s *tchanTracedServiceServer) handleStartTrace(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) {
var req TracedServiceStartTraceArgs
var res TracedServiceStartTraceResult
if err := req.Read(protocol); err != nil {
return false, nil, err
}
r, err :=
s.handler.StartTrace(ctx, req.Request)
if err != nil {
return false, nil, err
} else {
res.Success = r
}
return err == nil, &res, nil
}

View File

@ -0,0 +1,747 @@
// Autogenerated by Thrift Compiler (0.9.3)
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
package tracetest
import (
"bytes"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = bytes.Equal
type TracedService interface {
// Parameters:
// - Request
StartTrace(request *StartTraceRequest) (r *TraceResponse, err error)
// Parameters:
// - Request
JoinTrace(request *JoinTraceRequest) (r *TraceResponse, err error)
}
type TracedServiceClient struct {
Transport thrift.TTransport
ProtocolFactory thrift.TProtocolFactory
InputProtocol thrift.TProtocol
OutputProtocol thrift.TProtocol
SeqId int32
}
func NewTracedServiceClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *TracedServiceClient {
return &TracedServiceClient{Transport: t,
ProtocolFactory: f,
InputProtocol: f.GetProtocol(t),
OutputProtocol: f.GetProtocol(t),
SeqId: 0,
}
}
func NewTracedServiceClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *TracedServiceClient {
return &TracedServiceClient{Transport: t,
ProtocolFactory: nil,
InputProtocol: iprot,
OutputProtocol: oprot,
SeqId: 0,
}
}
// Parameters:
// - Request
func (p *TracedServiceClient) StartTrace(request *StartTraceRequest) (r *TraceResponse, err error) {
if err = p.sendStartTrace(request); err != nil {
return
}
return p.recvStartTrace()
}
func (p *TracedServiceClient) sendStartTrace(request *StartTraceRequest) (err error) {
oprot := p.OutputProtocol
if oprot == nil {
oprot = p.ProtocolFactory.GetProtocol(p.Transport)
p.OutputProtocol = oprot
}
p.SeqId++
if err = oprot.WriteMessageBegin("startTrace", thrift.CALL, p.SeqId); err != nil {
return
}
args := TracedServiceStartTraceArgs{
Request: request,
}
if err = args.Write(oprot); err != nil {
return
}
if err = oprot.WriteMessageEnd(); err != nil {
return
}
return oprot.Flush()
}
func (p *TracedServiceClient) recvStartTrace() (value *TraceResponse, err error) {
iprot := p.InputProtocol
if iprot == nil {
iprot = p.ProtocolFactory.GetProtocol(p.Transport)
p.InputProtocol = iprot
}
method, mTypeId, seqId, err := iprot.ReadMessageBegin()
if err != nil {
return
}
if method != "startTrace" {
err = thrift.NewTApplicationException(thrift.WRONG_METHOD_NAME, "startTrace failed: wrong method name")
return
}
if p.SeqId != seqId {
err = thrift.NewTApplicationException(thrift.BAD_SEQUENCE_ID, "startTrace failed: out of sequence response")
return
}
if mTypeId == thrift.EXCEPTION {
error0 := thrift.NewTApplicationException(thrift.UNKNOWN_APPLICATION_EXCEPTION, "Unknown Exception")
var error1 error
error1, err = error0.Read(iprot)
if err != nil {
return
}
if err = iprot.ReadMessageEnd(); err != nil {
return
}
err = error1
return
}
if mTypeId != thrift.REPLY {
err = thrift.NewTApplicationException(thrift.INVALID_MESSAGE_TYPE_EXCEPTION, "startTrace failed: invalid message type")
return
}
result := TracedServiceStartTraceResult{}
if err = result.Read(iprot); err != nil {
return
}
if err = iprot.ReadMessageEnd(); err != nil {
return
}
value = result.GetSuccess()
return
}
// Parameters:
// - Request
func (p *TracedServiceClient) JoinTrace(request *JoinTraceRequest) (r *TraceResponse, err error) {
if err = p.sendJoinTrace(request); err != nil {
return
}
return p.recvJoinTrace()
}
func (p *TracedServiceClient) sendJoinTrace(request *JoinTraceRequest) (err error) {
oprot := p.OutputProtocol
if oprot == nil {
oprot = p.ProtocolFactory.GetProtocol(p.Transport)
p.OutputProtocol = oprot
}
p.SeqId++
if err = oprot.WriteMessageBegin("joinTrace", thrift.CALL, p.SeqId); err != nil {
return
}
args := TracedServiceJoinTraceArgs{
Request: request,
}
if err = args.Write(oprot); err != nil {
return
}
if err = oprot.WriteMessageEnd(); err != nil {
return
}
return oprot.Flush()
}
func (p *TracedServiceClient) recvJoinTrace() (value *TraceResponse, err error) {
iprot := p.InputProtocol
if iprot == nil {
iprot = p.ProtocolFactory.GetProtocol(p.Transport)
p.InputProtocol = iprot
}
method, mTypeId, seqId, err := iprot.ReadMessageBegin()
if err != nil {
return
}
if method != "joinTrace" {
err = thrift.NewTApplicationException(thrift.WRONG_METHOD_NAME, "joinTrace failed: wrong method name")
return
}
if p.SeqId != seqId {
err = thrift.NewTApplicationException(thrift.BAD_SEQUENCE_ID, "joinTrace failed: out of sequence response")
return
}
if mTypeId == thrift.EXCEPTION {
error2 := thrift.NewTApplicationException(thrift.UNKNOWN_APPLICATION_EXCEPTION, "Unknown Exception")
var error3 error
error3, err = error2.Read(iprot)
if err != nil {
return
}
if err = iprot.ReadMessageEnd(); err != nil {
return
}
err = error3
return
}
if mTypeId != thrift.REPLY {
err = thrift.NewTApplicationException(thrift.INVALID_MESSAGE_TYPE_EXCEPTION, "joinTrace failed: invalid message type")
return
}
result := TracedServiceJoinTraceResult{}
if err = result.Read(iprot); err != nil {
return
}
if err = iprot.ReadMessageEnd(); err != nil {
return
}
value = result.GetSuccess()
return
}
type TracedServiceProcessor struct {
processorMap map[string]thrift.TProcessorFunction
handler TracedService
}
func (p *TracedServiceProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) {
p.processorMap[key] = processor
}
func (p *TracedServiceProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) {
processor, ok = p.processorMap[key]
return processor, ok
}
func (p *TracedServiceProcessor) ProcessorMap() map[string]thrift.TProcessorFunction {
return p.processorMap
}
func NewTracedServiceProcessor(handler TracedService) *TracedServiceProcessor {
self4 := &TracedServiceProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)}
self4.processorMap["startTrace"] = &tracedServiceProcessorStartTrace{handler: handler}
self4.processorMap["joinTrace"] = &tracedServiceProcessorJoinTrace{handler: handler}
return self4
}
func (p *TracedServiceProcessor) Process(iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) {
name, _, seqId, err := iprot.ReadMessageBegin()
if err != nil {
return false, err
}
if processor, ok := p.GetProcessorFunction(name); ok {
return processor.Process(seqId, iprot, oprot)
}
iprot.Skip(thrift.STRUCT)
iprot.ReadMessageEnd()
x5 := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name)
oprot.WriteMessageBegin(name, thrift.EXCEPTION, seqId)
x5.Write(oprot)
oprot.WriteMessageEnd()
oprot.Flush()
return false, x5
}
type tracedServiceProcessorStartTrace struct {
handler TracedService
}
func (p *tracedServiceProcessorStartTrace) Process(seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) {
args := TracedServiceStartTraceArgs{}
if err = args.Read(iprot); err != nil {
iprot.ReadMessageEnd()
x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error())
oprot.WriteMessageBegin("startTrace", thrift.EXCEPTION, seqId)
x.Write(oprot)
oprot.WriteMessageEnd()
oprot.Flush()
return false, err
}
iprot.ReadMessageEnd()
result := TracedServiceStartTraceResult{}
var retval *TraceResponse
var err2 error
if retval, err2 = p.handler.StartTrace(args.Request); err2 != nil {
x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing startTrace: "+err2.Error())
oprot.WriteMessageBegin("startTrace", thrift.EXCEPTION, seqId)
x.Write(oprot)
oprot.WriteMessageEnd()
oprot.Flush()
return true, err2
} else {
result.Success = retval
}
if err2 = oprot.WriteMessageBegin("startTrace", thrift.REPLY, seqId); err2 != nil {
err = err2
}
if err2 = result.Write(oprot); err == nil && err2 != nil {
err = err2
}
if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil {
err = err2
}
if err2 = oprot.Flush(); err == nil && err2 != nil {
err = err2
}
if err != nil {
return
}
return true, err
}
type tracedServiceProcessorJoinTrace struct {
handler TracedService
}
func (p *tracedServiceProcessorJoinTrace) Process(seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) {
args := TracedServiceJoinTraceArgs{}
if err = args.Read(iprot); err != nil {
iprot.ReadMessageEnd()
x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error())
oprot.WriteMessageBegin("joinTrace", thrift.EXCEPTION, seqId)
x.Write(oprot)
oprot.WriteMessageEnd()
oprot.Flush()
return false, err
}
iprot.ReadMessageEnd()
result := TracedServiceJoinTraceResult{}
var retval *TraceResponse
var err2 error
if retval, err2 = p.handler.JoinTrace(args.Request); err2 != nil {
x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing joinTrace: "+err2.Error())
oprot.WriteMessageBegin("joinTrace", thrift.EXCEPTION, seqId)
x.Write(oprot)
oprot.WriteMessageEnd()
oprot.Flush()
return true, err2
} else {
result.Success = retval
}
if err2 = oprot.WriteMessageBegin("joinTrace", thrift.REPLY, seqId); err2 != nil {
err = err2
}
if err2 = result.Write(oprot); err == nil && err2 != nil {
err = err2
}
if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil {
err = err2
}
if err2 = oprot.Flush(); err == nil && err2 != nil {
err = err2
}
if err != nil {
return
}
return true, err
}
// HELPER FUNCTIONS AND STRUCTURES
// Attributes:
// - Request
type TracedServiceStartTraceArgs struct {
Request *StartTraceRequest `thrift:"request,1" json:"request"`
}
func NewTracedServiceStartTraceArgs() *TracedServiceStartTraceArgs {
return &TracedServiceStartTraceArgs{}
}
var TracedServiceStartTraceArgs_Request_DEFAULT *StartTraceRequest
func (p *TracedServiceStartTraceArgs) GetRequest() *StartTraceRequest {
if !p.IsSetRequest() {
return TracedServiceStartTraceArgs_Request_DEFAULT
}
return p.Request
}
func (p *TracedServiceStartTraceArgs) IsSetRequest() bool {
return p.Request != nil
}
func (p *TracedServiceStartTraceArgs) Read(iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin()
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if err := p.readField1(iprot); err != nil {
return err
}
default:
if err := iprot.Skip(fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *TracedServiceStartTraceArgs) readField1(iprot thrift.TProtocol) error {
p.Request = &StartTraceRequest{}
if err := p.Request.Read(iprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Request), err)
}
return nil
}
func (p *TracedServiceStartTraceArgs) Write(oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin("startTrace_args"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if err := p.writeField1(oprot); err != nil {
return err
}
if err := oprot.WriteFieldStop(); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *TracedServiceStartTraceArgs) writeField1(oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin("request", thrift.STRUCT, 1); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:request: ", p), err)
}
if err := p.Request.Write(oprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Request), err)
}
if err := oprot.WriteFieldEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 1:request: ", p), err)
}
return err
}
func (p *TracedServiceStartTraceArgs) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("TracedServiceStartTraceArgs(%+v)", *p)
}
// Attributes:
// - Success
type TracedServiceStartTraceResult struct {
Success *TraceResponse `thrift:"success,0" json:"success,omitempty"`
}
func NewTracedServiceStartTraceResult() *TracedServiceStartTraceResult {
return &TracedServiceStartTraceResult{}
}
var TracedServiceStartTraceResult_Success_DEFAULT *TraceResponse
func (p *TracedServiceStartTraceResult) GetSuccess() *TraceResponse {
if !p.IsSetSuccess() {
return TracedServiceStartTraceResult_Success_DEFAULT
}
return p.Success
}
func (p *TracedServiceStartTraceResult) IsSetSuccess() bool {
return p.Success != nil
}
func (p *TracedServiceStartTraceResult) Read(iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin()
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 0:
if err := p.readField0(iprot); err != nil {
return err
}
default:
if err := iprot.Skip(fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *TracedServiceStartTraceResult) readField0(iprot thrift.TProtocol) error {
p.Success = &TraceResponse{}
if err := p.Success.Read(iprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Success), err)
}
return nil
}
func (p *TracedServiceStartTraceResult) Write(oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin("startTrace_result"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if err := p.writeField0(oprot); err != nil {
return err
}
if err := oprot.WriteFieldStop(); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *TracedServiceStartTraceResult) writeField0(oprot thrift.TProtocol) (err error) {
if p.IsSetSuccess() {
if err := oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 0:success: ", p), err)
}
if err := p.Success.Write(oprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Success), err)
}
if err := oprot.WriteFieldEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 0:success: ", p), err)
}
}
return err
}
func (p *TracedServiceStartTraceResult) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("TracedServiceStartTraceResult(%+v)", *p)
}
// Attributes:
// - Request
type TracedServiceJoinTraceArgs struct {
Request *JoinTraceRequest `thrift:"request,1" json:"request"`
}
func NewTracedServiceJoinTraceArgs() *TracedServiceJoinTraceArgs {
return &TracedServiceJoinTraceArgs{}
}
var TracedServiceJoinTraceArgs_Request_DEFAULT *JoinTraceRequest
func (p *TracedServiceJoinTraceArgs) GetRequest() *JoinTraceRequest {
if !p.IsSetRequest() {
return TracedServiceJoinTraceArgs_Request_DEFAULT
}
return p.Request
}
func (p *TracedServiceJoinTraceArgs) IsSetRequest() bool {
return p.Request != nil
}
func (p *TracedServiceJoinTraceArgs) Read(iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin()
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if err := p.readField1(iprot); err != nil {
return err
}
default:
if err := iprot.Skip(fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *TracedServiceJoinTraceArgs) readField1(iprot thrift.TProtocol) error {
p.Request = &JoinTraceRequest{}
if err := p.Request.Read(iprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Request), err)
}
return nil
}
func (p *TracedServiceJoinTraceArgs) Write(oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin("joinTrace_args"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if err := p.writeField1(oprot); err != nil {
return err
}
if err := oprot.WriteFieldStop(); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *TracedServiceJoinTraceArgs) writeField1(oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin("request", thrift.STRUCT, 1); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:request: ", p), err)
}
if err := p.Request.Write(oprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Request), err)
}
if err := oprot.WriteFieldEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 1:request: ", p), err)
}
return err
}
func (p *TracedServiceJoinTraceArgs) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("TracedServiceJoinTraceArgs(%+v)", *p)
}
// Attributes:
// - Success
type TracedServiceJoinTraceResult struct {
Success *TraceResponse `thrift:"success,0" json:"success,omitempty"`
}
func NewTracedServiceJoinTraceResult() *TracedServiceJoinTraceResult {
return &TracedServiceJoinTraceResult{}
}
var TracedServiceJoinTraceResult_Success_DEFAULT *TraceResponse
func (p *TracedServiceJoinTraceResult) GetSuccess() *TraceResponse {
if !p.IsSetSuccess() {
return TracedServiceJoinTraceResult_Success_DEFAULT
}
return p.Success
}
func (p *TracedServiceJoinTraceResult) IsSetSuccess() bool {
return p.Success != nil
}
func (p *TracedServiceJoinTraceResult) Read(iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin()
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 0:
if err := p.readField0(iprot); err != nil {
return err
}
default:
if err := iprot.Skip(fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *TracedServiceJoinTraceResult) readField0(iprot thrift.TProtocol) error {
p.Success = &TraceResponse{}
if err := p.Success.Read(iprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Success), err)
}
return nil
}
func (p *TracedServiceJoinTraceResult) Write(oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin("joinTrace_result"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if err := p.writeField0(oprot); err != nil {
return err
}
if err := oprot.WriteFieldStop(); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *TracedServiceJoinTraceResult) writeField0(oprot thrift.TProtocol) (err error) {
if p.IsSetSuccess() {
if err := oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 0:success: ", p), err)
}
if err := p.Success.Write(oprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Success), err)
}
if err := oprot.WriteFieldEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 0:success: ", p), err)
}
}
return err
}
func (p *TracedServiceJoinTraceResult) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("TracedServiceJoinTraceResult(%+v)", *p)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package jaeger implements an OpenTracing (http://opentracing.io) Tracer.
It is currently using Zipkin-compatible data model and can be directly
itegrated with Zipkin backend (http://zipkin.io).
For integration instructions please refer to the README:
https://github.com/uber/jaeger-client-go/blob/master/README.md
*/
package jaeger

View File

@ -0,0 +1,75 @@
hash: 4abcf83a509c003112c27a131eafde7d4fa11aeb17decb43ac0746593890ff85
updated: 2017-08-22T14:21:39.649697305-04:00
imports:
- name: github.com/apache/thrift
version: b2a4d4ae21c789b689dd162deb819665567f481c
subpackages:
- lib/go/thrift
- name: github.com/codahale/hdrhistogram
version: f8ad88b59a584afeee9d334eff879b104439117b
- name: github.com/crossdock/crossdock-go
version: 049aabb0122b03bc9bd30cab8f3f91fb60166361
subpackages:
- assert
- require
- name: github.com/davecgh/go-spew
version: adab96458c51a58dc1783b3335dcce5461522e75
subpackages:
- spew
- name: github.com/opentracing/opentracing-go
version: 1949ddbfd147afd4d964a9f00b24eb291e0e7c38
subpackages:
- ext
- log
- name: github.com/pmezard/go-difflib
version: 792786c7400a136282c1664665ae0a8db921c6c2
subpackages:
- difflib
- name: github.com/stretchr/testify
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
subpackages:
- assert
- require
- suite
- name: github.com/uber-go/atomic
version: 0c9e689d64f004564b79d9a663634756df322902
- name: github.com/uber/jaeger-lib
version: 575c678b2873b62aaadd0fa5817a2aeb3155dc4d
subpackages:
- metrics
- metrics/testutils
- name: github.com/uber/tchannel-go
version: a7ad9ecb640b5f10a0395b38d6319175172b3ab2
subpackages:
- atomic
- internal/argreader
- relay
- thrift
- thrift/gen-go/meta
- tnet
- tos
- trace/thrift/gen-go/tcollector
- trand
- typed
- name: go.uber.org/atomic
version: 4e336646b2ef9fc6e47be8e21594178f98e5ebcf
- name: go.uber.org/zap
version: 9cabc84638b70e564c3dab2766efcb1ded2aac9f
subpackages:
- buffer
- internal/bufferpool
- internal/color
- internal/exit
- internal/multierror
- zapcore
- name: golang.org/x/net
version: f5079bd7f6f74e23c4d65efa0f4ce14cbd6a3c0f
subpackages:
- bpf
- context
- context/ctxhttp
- internal/iana
- internal/socket
- ipv4
- ipv6
testImports: []

View File

@ -0,0 +1,34 @@
package: github.com/uber/jaeger-client-go
import:
- package: github.com/apache/thrift
version: ">=0.9.3, <0.11.0"
subpackages:
- lib/go/thrift
- package: github.com/opentracing/opentracing-go
version: ^1
subpackages:
- ext
- log
- package: golang.org/x/net
subpackages:
- context
- package: github.com/uber/tchannel-go
version: ^1.1.0
subpackages:
- atomic
- thrift
- thrift/gen-go/meta
- tnet
- trace/thrift/gen-go/tcollector
- typed
- package: github.com/stretchr/testify
version: ^1.1.3
subpackages:
- assert
- require
- suite
- package: github.com/crossdock/crossdock-go
- package: github.com/uber/jaeger-lib
version: ^1.0.0
subpackages:
- metrics

View File

@ -0,0 +1,64 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
// HeadersConfig contains the values for the header keys that Jaeger will use.
// These values may be either custom or default depending on whether custom
// values were provided via a configuration.
type HeadersConfig struct {
// JaegerDebugHeader is the name of HTTP header or a TextMap carrier key which,
// if found in the carrier, forces the trace to be sampled as "debug" trace.
// The value of the header is recorded as the tag on the root span, so that the
// trace can be found in the UI using this value as a correlation ID.
JaegerDebugHeader string `yaml:"jaegerDebugHeader"`
// JaegerBaggageHeader is the name of the HTTP header that is used to submit baggage.
// It differs from TraceBaggageHeaderPrefix in that it can be used only in cases where
// a root span does not exist.
JaegerBaggageHeader string `yaml:"jaegerBaggageHeader"`
// TraceContextHeaderName is the http header name used to propagate tracing context.
// This must be in lower-case to avoid mismatches when decoding incoming headers.
TraceContextHeaderName string `yaml:"TraceContextHeaderName"`
// TraceBaggageHeaderPrefix is the prefix for http headers used to propagate baggage.
// This must be in lower-case to avoid mismatches when decoding incoming headers.
TraceBaggageHeaderPrefix string `yaml:"traceBaggageHeaderPrefix"`
}
func (c *HeadersConfig) applyDefaults() *HeadersConfig {
if c.JaegerBaggageHeader == "" {
c.JaegerBaggageHeader = JaegerBaggageHeader
}
if c.JaegerDebugHeader == "" {
c.JaegerDebugHeader = JaegerDebugHeader
}
if c.TraceBaggageHeaderPrefix == "" {
c.TraceBaggageHeaderPrefix = TraceBaggageHeaderPrefix
}
if c.TraceContextHeaderName == "" {
c.TraceContextHeaderName = TraceContextHeaderName
}
return c
}
func getDefaultHeadersConfig() *HeadersConfig {
return &HeadersConfig{
JaegerDebugHeader: JaegerDebugHeader,
JaegerBaggageHeader: JaegerBaggageHeader,
TraceContextHeaderName: TraceContextHeaderName,
TraceBaggageHeaderPrefix: TraceBaggageHeaderPrefix,
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetDefaultOrCustom(t *testing.T) {
assert.Equal(t, (&HeadersConfig{}).applyDefaults(), getDefaultHeadersConfig())
assert.Equal(t, (&HeadersConfig{
JaegerDebugHeader: "custom-jaeger-debug-header",
}).applyDefaults(), &HeadersConfig{
JaegerDebugHeader: "custom-jaeger-debug-header",
JaegerBaggageHeader: JaegerBaggageHeader,
TraceContextHeaderName: TraceContextHeaderName,
TraceBaggageHeaderPrefix: TraceBaggageHeaderPrefix,
})
customHeaders := &HeadersConfig{
JaegerDebugHeader: "custom-jaeger-debug-header",
JaegerBaggageHeader: "custom-jaeger-baggage-header",
TraceContextHeaderName: "custom-tracer-state-header-name",
TraceBaggageHeaderPrefix: "custom-tracer-baggage-header-prefix",
}
assert.Equal(t, customHeaders.applyDefaults(), customHeaders)
}
func TestGetDefaultHeadersConfig(t *testing.T) {
assert.Equal(t, getDefaultHeadersConfig(), &HeadersConfig{
JaegerDebugHeader: JaegerDebugHeader,
JaegerBaggageHeader: JaegerBaggageHeader,
TraceContextHeaderName: TraceContextHeaderName,
TraceBaggageHeaderPrefix: TraceBaggageHeaderPrefix,
})
}

View File

@ -0,0 +1,101 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package remote
import (
"time"
"github.com/uber/jaeger-client-go"
)
const (
defaultMaxValueLength = 2048
defaultRefreshInterval = time.Minute
defaultHostPort = "localhost:5778"
)
// Option is a function that sets some option on the RestrictionManager
type Option func(options *options)
// Options is a factory for all available options
var Options options
type options struct {
denyBaggageOnInitializationFailure bool
metrics *jaeger.Metrics
logger jaeger.Logger
hostPort string
refreshInterval time.Duration
}
// DenyBaggageOnInitializationFailure creates an Option that determines the startup failure mode of RestrictionManager.
// If DenyBaggageOnInitializationFailure is true, RestrictionManager will not allow any baggage to be written until baggage
// restrictions have been retrieved from agent.
// If DenyBaggageOnInitializationFailure is false, RestrictionManager will allow any baggage to be written until baggage
// restrictions have been retrieved from agent.
func (options) DenyBaggageOnInitializationFailure(b bool) Option {
return func(o *options) {
o.denyBaggageOnInitializationFailure = b
}
}
// Metrics creates an Option that initializes Metrics on the RestrictionManager, which is used to emit statistics.
func (options) Metrics(m *jaeger.Metrics) Option {
return func(o *options) {
o.metrics = m
}
}
// Logger creates an Option that sets the logger used by the RestrictionManager.
func (options) Logger(logger jaeger.Logger) Option {
return func(o *options) {
o.logger = logger
}
}
// HostPort creates an Option that sets the hostPort of the local agent that contains the baggage restrictions.
func (options) HostPort(hostPort string) Option {
return func(o *options) {
o.hostPort = hostPort
}
}
// RefreshInterval creates an Option that sets how often the RestrictionManager will poll local agent for
// the baggage restrictions.
func (options) RefreshInterval(refreshInterval time.Duration) Option {
return func(o *options) {
o.refreshInterval = refreshInterval
}
}
func applyOptions(o ...Option) options {
opts := options{}
for _, option := range o {
option(&opts)
}
if opts.metrics == nil {
opts.metrics = jaeger.NewNullMetrics()
}
if opts.logger == nil {
opts.logger = jaeger.NullLogger
}
if opts.hostPort == "" {
opts.hostPort = defaultHostPort
}
if opts.refreshInterval == 0 {
opts.refreshInterval = defaultRefreshInterval
}
return opts
}

View File

@ -0,0 +1,157 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package remote
import (
"fmt"
"net/url"
"sync"
"time"
"github.com/uber/jaeger-client-go/internal/baggage"
thrift "github.com/uber/jaeger-client-go/thrift-gen/baggage"
"github.com/uber/jaeger-client-go/utils"
)
type httpBaggageRestrictionManagerProxy struct {
url string
}
func newHTTPBaggageRestrictionManagerProxy(hostPort, serviceName string) *httpBaggageRestrictionManagerProxy {
v := url.Values{}
v.Set("service", serviceName)
return &httpBaggageRestrictionManagerProxy{
url: fmt.Sprintf("http://%s/baggageRestrictions?%s", hostPort, v.Encode()),
}
}
func (s *httpBaggageRestrictionManagerProxy) GetBaggageRestrictions(serviceName string) ([]*thrift.BaggageRestriction, error) {
var out []*thrift.BaggageRestriction
if err := utils.GetJSON(s.url, &out); err != nil {
return nil, err
}
return out, nil
}
// RestrictionManager manages baggage restrictions by retrieving baggage restrictions from agent
type RestrictionManager struct {
options
mux sync.RWMutex
serviceName string
restrictions map[string]*baggage.Restriction
thriftProxy thrift.BaggageRestrictionManager
pollStopped sync.WaitGroup
stopPoll chan struct{}
invalidRestriction *baggage.Restriction
validRestriction *baggage.Restriction
// Determines if the manager has successfully retrieved baggage restrictions from agent
initialized bool
}
// NewRestrictionManager returns a BaggageRestrictionManager that polls the agent for the latest
// baggage restrictions.
func NewRestrictionManager(serviceName string, options ...Option) *RestrictionManager {
// TODO there is a developing use case where a single tracer can generate traces on behalf of many services.
// restrictionsMap will need to exist per service
opts := applyOptions(options...)
m := &RestrictionManager{
serviceName: serviceName,
options: opts,
restrictions: make(map[string]*baggage.Restriction),
thriftProxy: newHTTPBaggageRestrictionManagerProxy(opts.hostPort, serviceName),
stopPoll: make(chan struct{}),
invalidRestriction: baggage.NewRestriction(false, 0),
validRestriction: baggage.NewRestriction(true, defaultMaxValueLength),
}
m.pollStopped.Add(1)
go m.pollManager()
return m
}
// isReady returns true if the manager has retrieved baggage restrictions from the remote source.
func (m *RestrictionManager) isReady() bool {
m.mux.RLock()
defer m.mux.RUnlock()
return m.initialized
}
// GetRestriction implements RestrictionManager#GetRestriction.
func (m *RestrictionManager) GetRestriction(service, key string) *baggage.Restriction {
m.mux.RLock()
defer m.mux.RUnlock()
if !m.initialized {
if m.denyBaggageOnInitializationFailure {
return m.invalidRestriction
}
return m.validRestriction
}
if restriction, ok := m.restrictions[key]; ok {
return restriction
}
return m.invalidRestriction
}
// Close stops remote polling and closes the RemoteRestrictionManager.
func (m *RestrictionManager) Close() error {
close(m.stopPoll)
m.pollStopped.Wait()
return nil
}
func (m *RestrictionManager) pollManager() {
defer m.pollStopped.Done()
// attempt to initialize baggage restrictions
if err := m.updateRestrictions(); err != nil {
m.logger.Error(fmt.Sprintf("Failed to initialize baggage restrictions: %s", err.Error()))
}
ticker := time.NewTicker(m.refreshInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := m.updateRestrictions(); err != nil {
m.logger.Error(fmt.Sprintf("Failed to update baggage restrictions: %s", err.Error()))
}
case <-m.stopPoll:
return
}
}
}
func (m *RestrictionManager) updateRestrictions() error {
restrictions, err := m.thriftProxy.GetBaggageRestrictions(m.serviceName)
if err != nil {
m.metrics.BaggageRestrictionsUpdateFailure.Inc(1)
return err
}
newRestrictions := m.parseRestrictions(restrictions)
m.metrics.BaggageRestrictionsUpdateSuccess.Inc(1)
m.mux.Lock()
defer m.mux.Unlock()
m.initialized = true
m.restrictions = newRestrictions
return nil
}
func (m *RestrictionManager) parseRestrictions(restrictions []*thrift.BaggageRestriction) map[string]*baggage.Restriction {
setters := make(map[string]*baggage.Restriction, len(restrictions))
for _, restriction := range restrictions {
setters[restriction.BaggageKey] = baggage.NewRestriction(true, int(restriction.MaxValueLength))
}
return setters
}

View File

@ -0,0 +1,220 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package remote
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber-go/atomic"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-lib/metrics/testutils"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/internal/baggage"
thrift "github.com/uber/jaeger-client-go/thrift-gen/baggage"
)
const (
service = "svc"
expectedKey = "key"
expectedSize = 10
)
var (
testRestrictions = []*thrift.BaggageRestriction{
{BaggageKey: expectedKey, MaxValueLength: int32(expectedSize)},
}
)
var _ io.Closer = new(RestrictionManager) // API check
type baggageHandler struct {
returnError *atomic.Bool
restrictions []*thrift.BaggageRestriction
}
func (h *baggageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.returnError.Load() {
w.WriteHeader(http.StatusInternalServerError)
} else {
bytes, _ := json.Marshal(h.restrictions)
w.Header().Add("Content-Type", "application/json")
w.Write(bytes)
}
}
func (h *baggageHandler) setReturnError(b bool) {
h.returnError.Store(b)
}
func withHTTPServer(
restrictions []*thrift.BaggageRestriction,
f func(
metrics *jaeger.Metrics,
factory *metrics.LocalFactory,
handler *baggageHandler,
server *httptest.Server,
),
) {
factory := metrics.NewLocalFactory(0)
m := jaeger.NewMetrics(factory, nil)
handler := &baggageHandler{returnError: atomic.NewBool(true), restrictions: restrictions}
server := httptest.NewServer(handler)
defer server.Close()
f(m, factory, handler, server)
}
func TestNewRemoteRestrictionManager(t *testing.T) {
withHTTPServer(
testRestrictions,
func(
metrics *jaeger.Metrics,
factory *metrics.LocalFactory,
handler *baggageHandler,
server *httptest.Server,
) {
handler.setReturnError(false)
mgr := NewRestrictionManager(
service,
Options.HostPort(getHostPort(t, server.URL)),
Options.Metrics(metrics),
Options.Logger(jaeger.NullLogger),
)
defer mgr.Close()
for i := 0; i < 100; i++ {
if mgr.isReady() {
break
}
time.Sleep(time.Millisecond)
}
require.True(t, mgr.isReady())
restriction := mgr.GetRestriction(service, expectedKey)
assert.EqualValues(t, baggage.NewRestriction(true, expectedSize), restriction)
badKey := "bad-key"
restriction = mgr.GetRestriction(service, badKey)
assert.EqualValues(t, baggage.NewRestriction(false, 0), restriction)
testutils.AssertCounterMetrics(t, factory,
testutils.ExpectedMetric{
Name: "jaeger.baggage-restrictions-update",
Tags: map[string]string{"result": "ok"},
Value: 1,
},
)
})
}
func TestDenyBaggageOnInitializationFailure(t *testing.T) {
withHTTPServer(
testRestrictions,
func(
m *jaeger.Metrics,
factory *metrics.LocalFactory,
handler *baggageHandler,
server *httptest.Server,
) {
mgr := NewRestrictionManager(
service,
Options.DenyBaggageOnInitializationFailure(true),
Options.HostPort(getHostPort(t, server.URL)),
Options.Metrics(m),
Options.Logger(jaeger.NullLogger),
)
require.False(t, mgr.isReady())
metricName := "jaeger.baggage-restrictions-update"
metricTags := map[string]string{"result": "err"}
key := metrics.GetKey(metricName, metricTags, "|", "=")
for i := 0; i < 100; i++ {
// wait until the async initialization call is complete
counters, _ := factory.Snapshot()
if _, ok := counters[key]; ok {
break
}
time.Sleep(time.Millisecond)
}
testutils.AssertCounterMetrics(t, factory,
testutils.ExpectedMetric{
Name: metricName,
Tags: metricTags,
Value: 1,
},
)
// DenyBaggageOnInitializationFailure should not allow any key to be written
restriction := mgr.GetRestriction(service, expectedKey)
assert.EqualValues(t, baggage.NewRestriction(false, 0), restriction)
// have the http server return restrictions
handler.setReturnError(false)
mgr.updateRestrictions()
// Wait until manager retrieves baggage restrictions
for i := 0; i < 100; i++ {
if mgr.isReady() {
break
}
time.Sleep(time.Millisecond)
}
require.True(t, mgr.isReady())
restriction = mgr.GetRestriction(service, expectedKey)
assert.EqualValues(t, baggage.NewRestriction(true, expectedSize), restriction)
})
}
func TestAllowBaggageOnInitializationFailure(t *testing.T) {
withHTTPServer(
testRestrictions,
func(
metrics *jaeger.Metrics,
factory *metrics.LocalFactory,
handler *baggageHandler,
server *httptest.Server,
) {
mgr := NewRestrictionManager(
service,
Options.RefreshInterval(time.Millisecond),
Options.HostPort(getHostPort(t, server.URL)),
Options.Metrics(metrics),
Options.Logger(jaeger.NullLogger),
)
require.False(t, mgr.isReady())
// AllowBaggageOnInitializationFailure should allow any key to be written
restriction := mgr.GetRestriction(service, expectedKey)
assert.EqualValues(t, baggage.NewRestriction(true, 2048), restriction)
})
}
func getHostPort(t *testing.T, s string) string {
u, err := url.Parse(s)
require.NoError(t, err, "Failed to parse url")
return u.Host
}

View File

@ -0,0 +1,71 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package baggage
const (
defaultMaxValueLength = 2048
)
// Restriction determines whether a baggage key is allowed and contains any restrictions on the baggage value.
type Restriction struct {
keyAllowed bool
maxValueLength int
}
// NewRestriction returns a new Restriction.
func NewRestriction(keyAllowed bool, maxValueLength int) *Restriction {
return &Restriction{
keyAllowed: keyAllowed,
maxValueLength: maxValueLength,
}
}
// KeyAllowed returns whether the baggage key for this restriction is allowed.
func (r *Restriction) KeyAllowed() bool {
return r.keyAllowed
}
// MaxValueLength returns the max length for the baggage value.
func (r *Restriction) MaxValueLength() int {
return r.maxValueLength
}
// RestrictionManager keeps track of valid baggage keys and their restrictions. The manager
// will return a Restriction for a specific baggage key which will determine whether the baggage
// key is allowed for the current service and any other applicable restrictions on the baggage
// value.
type RestrictionManager interface {
GetRestriction(service, key string) *Restriction
}
// DefaultRestrictionManager allows any baggage key.
type DefaultRestrictionManager struct {
defaultRestriction *Restriction
}
// NewDefaultRestrictionManager returns a DefaultRestrictionManager.
func NewDefaultRestrictionManager(maxValueLength int) *DefaultRestrictionManager {
if maxValueLength == 0 {
maxValueLength = defaultMaxValueLength
}
return &DefaultRestrictionManager{
defaultRestriction: &Restriction{keyAllowed: true, maxValueLength: maxValueLength},
}
}
// GetRestriction implements RestrictionManager#GetRestriction.
func (m *DefaultRestrictionManager) GetRestriction(service, key string) *Restriction {
return m.defaultRestriction
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package baggage
import (
"testing"
"github.com/stretchr/testify/assert"
)
var _ RestrictionManager = &DefaultRestrictionManager{}
func TestDefaultRestrictionManager(t *testing.T) {
mgr := NewDefaultRestrictionManager(0)
restriction := mgr.GetRestriction("svc", "key")
assert.EqualValues(t, NewRestriction(true, 2048), restriction)
}

View File

@ -0,0 +1,81 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package spanlog
import (
"encoding/json"
"fmt"
"github.com/opentracing/opentracing-go/log"
)
type fieldsAsMap map[string]string
// MaterializeWithJSON converts log Fields into JSON string
// TODO refactor into pluggable materializer
func MaterializeWithJSON(logFields []log.Field) ([]byte, error) {
fields := fieldsAsMap(make(map[string]string, len(logFields)))
for _, field := range logFields {
field.Marshal(fields)
}
if event, ok := fields["event"]; ok && len(fields) == 1 {
return []byte(event), nil
}
return json.Marshal(fields)
}
func (ml fieldsAsMap) EmitString(key, value string) {
ml[key] = value
}
func (ml fieldsAsMap) EmitBool(key string, value bool) {
ml[key] = fmt.Sprintf("%t", value)
}
func (ml fieldsAsMap) EmitInt(key string, value int) {
ml[key] = fmt.Sprintf("%d", value)
}
func (ml fieldsAsMap) EmitInt32(key string, value int32) {
ml[key] = fmt.Sprintf("%d", value)
}
func (ml fieldsAsMap) EmitInt64(key string, value int64) {
ml[key] = fmt.Sprintf("%d", value)
}
func (ml fieldsAsMap) EmitUint32(key string, value uint32) {
ml[key] = fmt.Sprintf("%d", value)
}
func (ml fieldsAsMap) EmitUint64(key string, value uint64) {
ml[key] = fmt.Sprintf("%d", value)
}
func (ml fieldsAsMap) EmitFloat32(key string, value float32) {
ml[key] = fmt.Sprintf("%f", value)
}
func (ml fieldsAsMap) EmitFloat64(key string, value float64) {
ml[key] = fmt.Sprintf("%f", value)
}
func (ml fieldsAsMap) EmitObject(key string, value interface{}) {
ml[key] = fmt.Sprintf("%+v", value)
}
func (ml fieldsAsMap) EmitLazyLogger(value log.LazyLogger) {
value(ml)
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"github.com/opentracing/opentracing-go"
)
// TODO this file should not be needed after TChannel PR.
type formatKey int
// SpanContextFormat is a constant used as OpenTracing Format.
// Requires *SpanContext as carrier.
// This format is intended for interop with TChannel or other Zipkin-like tracers.
const SpanContextFormat formatKey = iota
type jaegerTraceContextPropagator struct {
tracer *Tracer
}
func (p *jaegerTraceContextPropagator) Inject(
ctx SpanContext,
abstractCarrier interface{},
) error {
carrier, ok := abstractCarrier.(*SpanContext)
if !ok {
return opentracing.ErrInvalidCarrier
}
carrier.CopyFrom(&ctx)
return nil
}
func (p *jaegerTraceContextPropagator) Extract(abstractCarrier interface{}) (SpanContext, error) {
carrier, ok := abstractCarrier.(*SpanContext)
if !ok {
return emptyContext, opentracing.ErrInvalidCarrier
}
ctx := new(SpanContext)
ctx.CopyFrom(carrier)
return *ctx, nil
}

View File

@ -0,0 +1,84 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"fmt"
"github.com/opentracing/opentracing-go/log"
j "github.com/uber/jaeger-client-go/thrift-gen/jaeger"
)
type tags []*j.Tag
// ConvertLogsToJaegerTags converts log Fields into jaeger tags.
func ConvertLogsToJaegerTags(logFields []log.Field) []*j.Tag {
fields := tags(make([]*j.Tag, 0, len(logFields)))
for _, field := range logFields {
field.Marshal(&fields)
}
return fields
}
func (t *tags) EmitString(key, value string) {
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_STRING, VStr: &value})
}
func (t *tags) EmitBool(key string, value bool) {
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_BOOL, VBool: &value})
}
func (t *tags) EmitInt(key string, value int) {
vLong := int64(value)
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_LONG, VLong: &vLong})
}
func (t *tags) EmitInt32(key string, value int32) {
vLong := int64(value)
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_LONG, VLong: &vLong})
}
func (t *tags) EmitInt64(key string, value int64) {
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_LONG, VLong: &value})
}
func (t *tags) EmitUint32(key string, value uint32) {
vLong := int64(value)
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_LONG, VLong: &vLong})
}
func (t *tags) EmitUint64(key string, value uint64) {
vLong := int64(value)
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_LONG, VLong: &vLong})
}
func (t *tags) EmitFloat32(key string, value float32) {
vDouble := float64(value)
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_DOUBLE, VDouble: &vDouble})
}
func (t *tags) EmitFloat64(key string, value float64) {
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_DOUBLE, VDouble: &value})
}
func (t *tags) EmitObject(key string, value interface{}) {
vStr := fmt.Sprintf("%+v", value)
*t = append(*t, &j.Tag{Key: key, VType: j.TagType_STRING, VStr: &vStr})
}
func (t *tags) EmitLazyLogger(value log.LazyLogger) {
value(t)
}

View File

@ -0,0 +1,172 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"time"
"github.com/opentracing/opentracing-go"
j "github.com/uber/jaeger-client-go/thrift-gen/jaeger"
"github.com/uber/jaeger-client-go/utils"
)
// BuildJaegerThrift builds jaeger span based on internal span.
func BuildJaegerThrift(span *Span) *j.Span {
startTime := utils.TimeToMicrosecondsSinceEpochInt64(span.startTime)
duration := span.duration.Nanoseconds() / int64(time.Microsecond)
jaegerSpan := &j.Span{
TraceIdLow: int64(span.context.traceID.Low),
TraceIdHigh: int64(span.context.traceID.High),
SpanId: int64(span.context.spanID),
ParentSpanId: int64(span.context.parentID),
OperationName: span.operationName,
Flags: int32(span.context.flags),
StartTime: startTime,
Duration: duration,
Tags: buildTags(span.tags),
Logs: buildLogs(span.logs),
References: buildReferences(span.references),
}
return jaegerSpan
}
// BuildJaegerProcessThrift creates a thrift Process type.
func BuildJaegerProcessThrift(span *Span) *j.Process {
return buildJaegerProcessThrift(span.tracer)
}
func buildJaegerProcessThrift(tracer *Tracer) *j.Process {
process := &j.Process{
ServiceName: tracer.serviceName,
Tags: buildTags(tracer.tags),
}
return process
}
func buildTags(tags []Tag) []*j.Tag {
jTags := make([]*j.Tag, 0, len(tags))
for _, tag := range tags {
jTag := buildTag(&tag)
jTags = append(jTags, jTag)
}
return jTags
}
func buildLogs(logs []opentracing.LogRecord) []*j.Log {
jLogs := make([]*j.Log, 0, len(logs))
for _, log := range logs {
jLog := &j.Log{
Timestamp: utils.TimeToMicrosecondsSinceEpochInt64(log.Timestamp),
Fields: ConvertLogsToJaegerTags(log.Fields),
}
jLogs = append(jLogs, jLog)
}
return jLogs
}
func buildTag(tag *Tag) *j.Tag {
jTag := &j.Tag{Key: tag.key}
switch value := tag.value.(type) {
case string:
vStr := truncateString(value)
jTag.VStr = &vStr
jTag.VType = j.TagType_STRING
case []byte:
if len(value) > maxAnnotationLength {
value = value[:maxAnnotationLength]
}
jTag.VBinary = value
jTag.VType = j.TagType_BINARY
case int:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case uint:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case int8:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case uint8:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case int16:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case uint16:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case int32:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case uint32:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case int64:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case uint64:
vLong := int64(value)
jTag.VLong = &vLong
jTag.VType = j.TagType_LONG
case float32:
vDouble := float64(value)
jTag.VDouble = &vDouble
jTag.VType = j.TagType_DOUBLE
case float64:
vDouble := float64(value)
jTag.VDouble = &vDouble
jTag.VType = j.TagType_DOUBLE
case bool:
vBool := value
jTag.VBool = &vBool
jTag.VType = j.TagType_BOOL
default:
vStr := truncateString(stringify(value))
jTag.VStr = &vStr
jTag.VType = j.TagType_STRING
}
return jTag
}
func buildReferences(references []Reference) []*j.SpanRef {
retMe := make([]*j.SpanRef, 0, len(references))
for _, ref := range references {
if ref.Type == opentracing.ChildOfRef {
retMe = append(retMe, spanRef(ref.Context, j.SpanRefType_CHILD_OF))
} else if ref.Type == opentracing.FollowsFromRef {
retMe = append(retMe, spanRef(ref.Context, j.SpanRefType_FOLLOWS_FROM))
}
}
return retMe
}
func spanRef(ctx SpanContext, refType j.SpanRefType) *j.SpanRef {
return &j.SpanRef{
RefType: refType,
TraceIdLow: int64(ctx.traceID.Low),
TraceIdHigh: int64(ctx.traceID.High),
SpanId: int64(ctx.spanID),
}
}

View File

@ -0,0 +1,388 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"errors"
"fmt"
"testing"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
j "github.com/uber/jaeger-client-go/thrift-gen/jaeger"
"github.com/uber/jaeger-client-go/utils"
)
var (
someString = "str"
someBool = true
someLong = int64(123)
someDouble = float64(123)
someBinary = []byte("hello")
someSlice = []string{"a"}
someSliceString = "[a]"
)
func TestBuildJaegerThrift(t *testing.T) {
tracer, closer := NewTracer("DOOP",
NewConstSampler(true),
NewNullReporter())
defer closer.Close()
sp1 := tracer.StartSpan("sp1").(*Span)
ext.SpanKindRPCServer.Set(sp1)
ext.PeerService.Set(sp1, "svc")
sp2 := tracer.StartSpan("sp2", opentracing.ChildOf(sp1.Context())).(*Span)
ext.SpanKindRPCClient.Set(sp2)
sp2.Finish()
sp1.Finish()
jaegerSpan1 := BuildJaegerThrift(sp1)
jaegerSpan2 := BuildJaegerThrift(sp2)
assert.Equal(t, "sp1", jaegerSpan1.OperationName)
assert.Equal(t, "sp2", jaegerSpan2.OperationName)
assert.EqualValues(t, 0, jaegerSpan1.ParentSpanId)
assert.Equal(t, jaegerSpan1.SpanId, jaegerSpan2.ParentSpanId)
assert.Len(t, jaegerSpan1.Tags, 4)
tag := findTag(jaegerSpan1, SamplerTypeTagKey)
assert.Equal(t, SamplerTypeConst, *tag.VStr)
tag = findTag(jaegerSpan1, string(ext.SpanKind))
assert.Equal(t, string(ext.SpanKindRPCServerEnum), *tag.VStr)
tag = findTag(jaegerSpan1, string(ext.PeerService))
assert.Equal(t, "svc", *tag.VStr)
assert.Empty(t, jaegerSpan1.References)
assert.Len(t, jaegerSpan2.References, 1)
assert.Equal(t, j.SpanRefType_CHILD_OF, jaegerSpan2.References[0].RefType)
assert.EqualValues(t, jaegerSpan1.TraceIdLow, jaegerSpan2.References[0].TraceIdLow)
assert.EqualValues(t, jaegerSpan1.TraceIdHigh, jaegerSpan2.References[0].TraceIdHigh)
assert.EqualValues(t, jaegerSpan1.SpanId, jaegerSpan2.References[0].SpanId)
tag = findTag(jaegerSpan2, string(ext.SpanKind))
assert.Equal(t, string(ext.SpanKindRPCClientEnum), *tag.VStr)
}
func TestBuildJaegerProcessThrift(t *testing.T) {
tracer, closer := NewTracer("DOOP",
NewConstSampler(true),
NewNullReporter())
defer closer.Close()
sp := tracer.StartSpan("sp1").(*Span)
sp.Finish()
process := BuildJaegerProcessThrift(sp)
assert.Equal(t, process.ServiceName, "DOOP")
require.Len(t, process.Tags, 3)
assert.NotNil(t, findJaegerTag("jaeger.version", process.Tags))
assert.NotNil(t, findJaegerTag("hostname", process.Tags))
assert.NotNil(t, findJaegerTag("ip", process.Tags))
}
func TestBuildLogs(t *testing.T) {
tracer, closer := NewTracer("DOOP",
NewConstSampler(true),
NewNullReporter())
defer closer.Close()
root := tracer.StartSpan("s1")
someTime := time.Now().Add(-time.Minute)
someTimeInt64 := utils.TimeToMicrosecondsSinceEpochInt64(someTime)
errString := "error"
tests := []struct {
field log.Field
logFunc func(sp opentracing.Span)
expected []*j.Tag
expectedTimestamp int64
disableSampling bool
}{
{field: log.String("event", someString), expected: []*j.Tag{{Key: "event", VType: j.TagType_STRING, VStr: &someString}}},
{field: log.String("k", someString), expected: []*j.Tag{{Key: "k", VType: j.TagType_STRING, VStr: &someString}}},
{field: log.Bool("k", someBool), expected: []*j.Tag{{Key: "k", VType: j.TagType_BOOL, VBool: &someBool}}},
{field: log.Int("k", 123), expected: []*j.Tag{{Key: "k", VType: j.TagType_LONG, VLong: &someLong}}},
{field: log.Int32("k", 123), expected: []*j.Tag{{Key: "k", VType: j.TagType_LONG, VLong: &someLong}}},
{field: log.Int64("k", 123), expected: []*j.Tag{{Key: "k", VType: j.TagType_LONG, VLong: &someLong}}},
{field: log.Uint32("k", 123), expected: []*j.Tag{{Key: "k", VType: j.TagType_LONG, VLong: &someLong}}},
{field: log.Uint64("k", 123), expected: []*j.Tag{{Key: "k", VType: j.TagType_LONG, VLong: &someLong}}},
{field: log.Float32("k", 123), expected: []*j.Tag{{Key: "k", VType: j.TagType_DOUBLE, VDouble: &someDouble}}},
{field: log.Float64("k", 123), expected: []*j.Tag{{Key: "k", VType: j.TagType_DOUBLE, VDouble: &someDouble}}},
{field: log.Error(errors.New(errString)), expected: []*j.Tag{{Key: "error", VType: j.TagType_STRING, VStr: &errString}}},
{field: log.Object("k", someSlice), expected: []*j.Tag{{Key: "k", VType: j.TagType_STRING, VStr: &someSliceString}}},
{
field: log.Lazy(func(fv log.Encoder) {
fv.EmitBool("k", someBool)
}),
expected: []*j.Tag{{Key: "k", VType: j.TagType_BOOL, VBool: &someBool}},
},
{
logFunc: func(sp opentracing.Span) {
sp.LogKV("event", someString)
},
expected: []*j.Tag{{Key: "event", VType: j.TagType_STRING, VStr: &someString}},
},
{
logFunc: func(sp opentracing.Span) {
sp.LogKV("non-even number of arguments")
},
// this is a bit fragile, but ¯\_(ツ)_/¯
expected: []*j.Tag{
{Key: "error", VType: j.TagType_STRING, VStr: getStringPtr("non-even keyValues len: 1")},
{Key: "function", VType: j.TagType_STRING, VStr: getStringPtr("LogKV")},
},
},
{
logFunc: func(sp opentracing.Span) {
sp.LogEvent(someString)
},
expected: []*j.Tag{{Key: "event", VType: j.TagType_STRING, VStr: &someString}},
},
{
logFunc: func(sp opentracing.Span) {
sp.LogEventWithPayload(someString, "payload")
},
expected: []*j.Tag{
{Key: "event", VType: j.TagType_STRING, VStr: &someString},
{Key: "payload", VType: j.TagType_STRING, VStr: getStringPtr("payload")},
},
},
{
logFunc: func(sp opentracing.Span) {
sp.Log(opentracing.LogData{Event: someString})
},
expected: []*j.Tag{{Key: "event", VType: j.TagType_STRING, VStr: &someString}},
},
{
logFunc: func(sp opentracing.Span) {
sp.Log(opentracing.LogData{Event: someString, Payload: "payload"})
},
expected: []*j.Tag{
{Key: "event", VType: j.TagType_STRING, VStr: &someString},
{Key: "payload", VType: j.TagType_STRING, VStr: getStringPtr("payload")},
},
},
{
logFunc: func(sp opentracing.Span) {
sp.FinishWithOptions(opentracing.FinishOptions{
LogRecords: []opentracing.LogRecord{
{
Timestamp: someTime,
Fields: []log.Field{log.String("event", someString)},
},
},
})
},
expected: []*j.Tag{{Key: "event", VType: j.TagType_STRING, VStr: &someString}},
expectedTimestamp: someTimeInt64,
},
{
logFunc: func(sp opentracing.Span) {
sp.FinishWithOptions(opentracing.FinishOptions{
BulkLogData: []opentracing.LogData{
{
Timestamp: someTime,
Event: someString,
},
},
})
},
expected: []*j.Tag{{Key: "event", VType: j.TagType_STRING, VStr: &someString}},
expectedTimestamp: someTimeInt64,
},
{
logFunc: func(sp opentracing.Span) {
sp.FinishWithOptions(opentracing.FinishOptions{
BulkLogData: []opentracing.LogData{
{
Timestamp: someTime,
Event: someString,
Payload: "payload",
},
},
})
},
expected: []*j.Tag{
{Key: "event", VType: j.TagType_STRING, VStr: &someString},
{Key: "payload", VType: j.TagType_STRING, VStr: getStringPtr("payload")},
},
expectedTimestamp: someTimeInt64,
},
{
disableSampling: true,
field: log.String("event", someString),
},
{
disableSampling: true,
logFunc: func(sp opentracing.Span) {
sp.LogKV("event", someString)
},
},
}
for i, test := range tests {
testName := fmt.Sprintf("test-%02d", i)
sp := tracer.StartSpan(testName, opentracing.ChildOf(root.Context()))
if test.disableSampling {
ext.SamplingPriority.Set(sp, 0)
}
if test.logFunc != nil {
test.logFunc(sp)
} else if test.field != (log.Field{}) {
sp.LogFields(test.field)
}
jaegerSpan := BuildJaegerThrift(sp.(*Span))
if test.disableSampling {
assert.Equal(t, 0, len(jaegerSpan.Logs), testName)
continue
}
assert.Equal(t, 1, len(jaegerSpan.Logs), testName)
compareTagSlices(t, test.expected, jaegerSpan.Logs[0].GetFields(), testName)
if test.expectedTimestamp != 0 {
assert.Equal(t, test.expectedTimestamp, jaegerSpan.Logs[0].Timestamp, testName)
}
}
}
func TestBuildTags(t *testing.T) {
tests := []struct {
tag Tag
expected *j.Tag
}{
{tag: Tag{key: "k", value: someString}, expected: &j.Tag{Key: "k", VType: j.TagType_STRING, VStr: &someString}},
{tag: Tag{key: "k", value: int(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: uint(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: int8(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: uint8(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: int16(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: uint16(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: int32(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: uint32(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: int64(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: uint64(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_LONG, VLong: &someLong}},
{tag: Tag{key: "k", value: float32(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_DOUBLE, VDouble: &someDouble}},
{tag: Tag{key: "k", value: float64(123)}, expected: &j.Tag{Key: "k", VType: j.TagType_DOUBLE, VDouble: &someDouble}},
{tag: Tag{key: "k", value: someBool}, expected: &j.Tag{Key: "k", VType: j.TagType_BOOL, VBool: &someBool}},
{tag: Tag{key: "k", value: someBinary}, expected: &j.Tag{Key: "k", VType: j.TagType_BINARY, VBinary: someBinary}},
{tag: Tag{key: "k", value: someSlice}, expected: &j.Tag{Key: "k", VType: j.TagType_STRING, VStr: &someSliceString}},
}
for i, test := range tests {
testName := fmt.Sprintf("test-%02d", i)
actual := buildTags([]Tag{test.tag})
assert.Len(t, actual, 1)
compareTags(t, test.expected, actual[0], testName)
}
}
func TestBuildReferences(t *testing.T) {
references := []Reference{
{Type: opentracing.ChildOfRef, Context: SpanContext{traceID: TraceID{High: 1, Low: 1}, spanID: SpanID(1)}},
{Type: opentracing.FollowsFromRef, Context: SpanContext{traceID: TraceID{High: 2, Low: 2}, spanID: SpanID(2)}},
}
spanRefs := buildReferences(references)
assert.Len(t, spanRefs, 2)
assert.Equal(t, j.SpanRefType_CHILD_OF, spanRefs[0].RefType)
assert.EqualValues(t, 1, spanRefs[0].SpanId)
assert.EqualValues(t, 1, spanRefs[0].TraceIdHigh)
assert.EqualValues(t, 1, spanRefs[0].TraceIdLow)
assert.Equal(t, j.SpanRefType_FOLLOWS_FROM, spanRefs[1].RefType)
assert.EqualValues(t, 2, spanRefs[1].SpanId)
assert.EqualValues(t, 2, spanRefs[1].TraceIdHigh)
assert.EqualValues(t, 2, spanRefs[1].TraceIdLow)
}
func TestJaegerSpanBaggageLogs(t *testing.T) {
tracer, closer := NewTracer("DOOP",
NewConstSampler(true),
NewNullReporter())
defer closer.Close()
sp := tracer.StartSpan("s1").(*Span)
sp.SetBaggageItem("auth.token", "token")
ext.SpanKindRPCServer.Set(sp)
sp.Finish()
jaegerSpan := BuildJaegerThrift(sp)
require.Len(t, jaegerSpan.Logs, 1)
fields := jaegerSpan.Logs[0].Fields
require.Len(t, fields, 3)
assertJaegerTag(t, fields, "event", "baggage")
assertJaegerTag(t, fields, "key", "auth.token")
assertJaegerTag(t, fields, "value", "token")
}
func assertJaegerTag(t *testing.T, tags []*j.Tag, key string, value string) {
tag := findJaegerTag(key, tags)
require.NotNil(t, tag)
assert.Equal(t, value, tag.GetVStr())
}
func getStringPtr(s string) *string {
return &s
}
func findTag(span *j.Span, key string) *j.Tag {
for _, s := range span.Tags {
if s.Key == key {
return s
}
}
return nil
}
func findJaegerTag(key string, tags []*j.Tag) *j.Tag {
for _, tag := range tags {
if tag.Key == key {
return tag
}
}
return nil
}
func compareTagSlices(t *testing.T, expectedTags, actualTags []*j.Tag, testName string) {
assert.Equal(t, len(expectedTags), len(actualTags))
for _, expectedTag := range expectedTags {
actualTag := findJaegerTag(expectedTag.Key, actualTags)
compareTags(t, expectedTag, actualTag, testName)
}
}
func compareTags(t *testing.T, expected, actual *j.Tag, testName string) {
if expected == nil && actual == nil {
return
}
if expected == nil || actual == nil {
assert.Fail(t, "one of the tags is nil", testName)
return
}
assert.Equal(t, expected.Key, actual.Key, testName)
assert.Equal(t, expected.VType, actual.VType, testName)
switch expected.VType {
case j.TagType_STRING:
assert.Equal(t, *expected.VStr, *actual.VStr, testName)
case j.TagType_LONG:
assert.Equal(t, *expected.VLong, *actual.VLong, testName)
case j.TagType_DOUBLE:
assert.Equal(t, *expected.VDouble, *actual.VDouble, testName)
case j.TagType_BOOL:
assert.Equal(t, *expected.VBool, *actual.VBool, testName)
case j.TagType_BINARY:
assert.Equal(t, expected.VBinary, actual.VBinary, testName)
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import "log"
// Logger provides an abstract interface for logging from Reporters.
// Applications can provide their own implementation of this interface to adapt
// reporters logging to whatever logging library they prefer (stdlib log,
// logrus, go-logging, etc).
type Logger interface {
// Error logs a message at error priority
Error(msg string)
// Infof logs a message at info priority
Infof(msg string, args ...interface{})
}
// StdLogger is implementation of the Logger interface that delegates to default `log` package
var StdLogger = &stdLogger{}
type stdLogger struct{}
func (l *stdLogger) Error(msg string) {
log.Printf("ERROR: %s", msg)
}
// Infof logs a message at info priority
func (l *stdLogger) Infof(msg string, args ...interface{}) {
log.Printf(msg, args...)
}
// NullLogger is implementation of the Logger interface that delegates to default `log` package
var NullLogger = &nullLogger{}
type nullLogger struct{}
func (l *nullLogger) Error(msg string) {}
func (l *nullLogger) Infof(msg string, args ...interface{}) {}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package log
import (
"testing"
)
func TestLogger(t *testing.T) {
for _, logger := range []Logger{StdLogger, NullLogger} {
logger.Infof("Hi %s", "there")
logger.Error("Bad wolf")
}
}

View File

@ -0,0 +1,60 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zap
import (
"context"
"fmt"
jaeger "github.com/uber/jaeger-client-go"
opentracing "github.com/opentracing/opentracing-go"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Trace creates a field that extracts tracing information from a context and
// includes it under the "trace" key.
//
// Because the opentracing APIs don't expose this information, the returned
// zap.Field is a no-op for contexts that don't contain a span or contain a
// non-Jaeger span.
func Trace(ctx context.Context) zapcore.Field {
if ctx == nil {
return zap.Skip()
}
return zap.Object("trace", trace{ctx})
}
type trace struct {
ctx context.Context
}
func (t trace) MarshalLogObject(enc zapcore.ObjectEncoder) error {
span := opentracing.SpanFromContext(t.ctx)
if span == nil {
return nil
}
j, ok := span.Context().(jaeger.SpanContext)
if !ok {
return nil
}
if !j.IsValid() {
return fmt.Errorf("invalid span: %v", j.SpanID())
}
enc.AddString("span", j.SpanID().String())
enc.AddString("trace", j.TraceID().String())
return nil
}

View File

@ -0,0 +1,64 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zap
import (
"context"
"testing"
jaeger "github.com/uber/jaeger-client-go"
opentracing "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func TestTraceField(t *testing.T) {
assert.Equal(t, zap.Skip(), Trace(nil), "Expected Trace of a nil context to be a no-op.")
withTracedContext(func(ctx context.Context) {
enc := zapcore.NewMapObjectEncoder()
Trace(ctx).AddTo(enc)
logged, ok := enc.Fields["trace"].(map[string]interface{})
require.True(t, ok, "Expected trace to be a map.")
// We could extract the span from the context and assert specific IDs,
// but that just copies the production code. Instead, just assert that
// the keys we expect are present.
keys := make(map[string]struct{}, len(logged))
for k := range logged {
keys[k] = struct{}{}
}
assert.Equal(
t,
map[string]struct{}{"span": {}, "trace": {}},
keys,
"Expected to log span and trace IDs.",
)
})
}
func withTracedContext(f func(ctx context.Context)) {
tracer, closer := jaeger.NewTracer(
"serviceName", jaeger.NewConstSampler(true), jaeger.NewNullReporter(),
)
defer closer.Close()
ctx := opentracing.ContextWithSpan(context.Background(), tracer.StartSpan("test"))
f(ctx)
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zap
import (
"go.uber.org/zap"
)
// Logger is an adapter from zap Logger to jaeger-lib Logger.
type Logger struct {
logger *zap.SugaredLogger
}
// NewLogger creates a new Logger.
func NewLogger(logger *zap.Logger) *Logger {
return &Logger{logger: logger.Sugar()}
}
// Error logs a message at error priority
func (l *Logger) Error(msg string) {
l.logger.Error(msg)
}
// Infof logs a message at info priority
func (l *Logger) Infof(msg string, args ...interface{}) {
l.logger.Infof(msg, args...)
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zap
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func TestLogger(t *testing.T) {
buf := &bytes.Buffer{}
encoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{MessageKey: "key"})
logger := NewLogger(zap.New(zapcore.NewCore(encoder, zapcore.AddSync(buf), zapcore.InfoLevel)))
logger.Infof("Hi %s %d", "there", 5)
assert.Equal(t, buf.String(), "Hi there 5\n")
buf.Reset()
logger.Error("Bad wolf")
assert.Equal(t, buf.String(), "Bad wolf\n")
}

View File

@ -0,0 +1,53 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import "log"
// NB This will be deprecated in 3.0.0, please use jaeger-client-go/log/logger instead.
// Logger provides an abstract interface for logging from Reporters.
// Applications can provide their own implementation of this interface to adapt
// reporters logging to whatever logging library they prefer (stdlib log,
// logrus, go-logging, etc).
type Logger interface {
// Error logs a message at error priority
Error(msg string)
// Infof logs a message at info priority
Infof(msg string, args ...interface{})
}
// StdLogger is implementation of the Logger interface that delegates to default `log` package
var StdLogger = &stdLogger{}
type stdLogger struct{}
func (l *stdLogger) Error(msg string) {
log.Printf("ERROR: %s", msg)
}
// Infof logs a message at info priority
func (l *stdLogger) Infof(msg string, args ...interface{}) {
log.Printf(msg, args...)
}
// NullLogger is implementation of the Logger interface that delegates to default `log` package
var NullLogger = &nullLogger{}
type nullLogger struct{}
func (l *nullLogger) Error(msg string) {}
func (l *nullLogger) Infof(msg string, args ...interface{}) {}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"testing"
"github.com/uber/jaeger-client-go/log"
)
func TestLogger(t *testing.T) {
for _, logger := range []Logger{StdLogger, NullLogger} {
logger.Infof("Hi %s", "there")
logger.Error("Bad wolf")
}
}
func TestCompatibility(t *testing.T) {
for _, logger := range []log.Logger{StdLogger, NullLogger} {
logger.Infof("Hi %s", "there")
logger.Error("Bad wolf")
}
for _, logger := range []Logger{log.StdLogger, log.NullLogger} {
logger.Infof("Hi %s", "there")
logger.Error("Bad wolf")
}
}

View File

@ -0,0 +1,100 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"github.com/uber/jaeger-lib/metrics"
)
// Metrics is a container of all stats emitted by Jaeger tracer.
type Metrics struct {
// Number of traces started by this tracer as sampled
TracesStartedSampled metrics.Counter `metric:"traces" tags:"state=started,sampled=y"`
// Number of traces started by this tracer as not sampled
TracesStartedNotSampled metrics.Counter `metric:"traces" tags:"state=started,sampled=n"`
// Number of externally started sampled traces this tracer joined
TracesJoinedSampled metrics.Counter `metric:"traces" tags:"state=joined,sampled=y"`
// Number of externally started not-sampled traces this tracer joined
TracesJoinedNotSampled metrics.Counter `metric:"traces" tags:"state=joined,sampled=n"`
// Number of sampled spans started by this tracer
SpansStarted metrics.Counter `metric:"spans" tags:"group=lifecycle,state=started"`
// Number of sampled spans finished by this tracer
SpansFinished metrics.Counter `metric:"spans" tags:"group=lifecycle,state=finished"`
// Number of sampled spans started by this tracer
SpansSampled metrics.Counter `metric:"spans" tags:"group=sampling,sampled=y"`
// Number of not-sampled spans started by this tracer
SpansNotSampled metrics.Counter `metric:"spans" tags:"group=sampling,sampled=n"`
// Number of errors decoding tracing context
DecodingErrors metrics.Counter `metric:"decoding-errors"`
// Number of spans successfully reported
ReporterSuccess metrics.Counter `metric:"reporter-spans" tags:"state=success"`
// Number of spans in failed attempts to report
ReporterFailure metrics.Counter `metric:"reporter-spans" tags:"state=failure"`
// Number of spans dropped due to internal queue overflow
ReporterDropped metrics.Counter `metric:"reporter-spans" tags:"state=dropped"`
// Current number of spans in the reporter queue
ReporterQueueLength metrics.Gauge `metric:"reporter-queue"`
// Number of times the Sampler succeeded to retrieve sampling strategy
SamplerRetrieved metrics.Counter `metric:"sampler" tags:"state=retrieved"`
// Number of times the Sampler succeeded to retrieve and update sampling strategy
SamplerUpdated metrics.Counter `metric:"sampler" tags:"state=updated"`
// Number of times the Sampler failed to update sampling strategy
SamplerUpdateFailure metrics.Counter `metric:"sampler" tags:"state=failure,phase=updating"`
// Number of times the Sampler failed to retrieve sampling strategy
SamplerQueryFailure metrics.Counter `metric:"sampler" tags:"state=failure,phase=query"`
// Number of times baggage was successfully written or updated on spans.
BaggageUpdateSuccess metrics.Counter `metric:"baggage-update" tags:"result=ok"`
// Number of times baggage failed to write or update on spans.
BaggageUpdateFailure metrics.Counter `metric:"baggage-update" tags:"result=err"`
// Number of times baggage was truncated as per baggage restrictions.
BaggageTruncate metrics.Counter `metric:"baggage-truncate"`
// Number of times baggage restrictions were successfully updated.
BaggageRestrictionsUpdateSuccess metrics.Counter `metric:"baggage-restrictions-update" tags:"result=ok"`
// Number of times baggage restrictions failed to update.
BaggageRestrictionsUpdateFailure metrics.Counter `metric:"baggage-restrictions-update" tags:"result=err"`
}
// NewMetrics creates a new Metrics struct and initializes it.
func NewMetrics(factory metrics.Factory, globalTags map[string]string) *Metrics {
m := &Metrics{}
metrics.Init(m, factory.Namespace("jaeger", nil), globalTags)
return m
}
// NewNullMetrics creates a new Metrics struct that won't report any metrics.
func NewNullMetrics() *Metrics {
return NewMetrics(metrics.NullFactory, nil)
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-lib/metrics/testutils"
)
func TestNewMetrics(t *testing.T) {
tags := map[string]string{"lib": "jaeger"}
factory := metrics.NewLocalFactory(0)
m := NewMetrics(factory, tags)
require.NotNil(t, m.SpansSampled, "counter not initialized")
require.NotNil(t, m.ReporterQueueLength, "gauge not initialized")
m.SpansSampled.Inc(1)
m.ReporterQueueLength.Update(11)
testutils.AssertCounterMetrics(t, factory,
testutils.ExpectedMetric{
Name: "jaeger.spans",
Tags: map[string]string{"group": "sampling", "lib": "jaeger", "sampled": "y"},
Value: 1,
},
)
testutils.AssertGaugeMetrics(t, factory,
testutils.ExpectedMetric{
Name: "jaeger.reporter-queue",
Tags: map[string]string{"lib": "jaeger"},
Value: 11,
},
)
}

View File

@ -0,0 +1,88 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import opentracing "github.com/opentracing/opentracing-go"
// Observer can be registered with the Tracer to receive notifications about
// new Spans.
//
// Deprecated: use jaeger.ContribObserver instead.
type Observer interface {
OnStartSpan(operationName string, options opentracing.StartSpanOptions) SpanObserver
}
// SpanObserver is created by the Observer and receives notifications about
// other Span events.
//
// Deprecated: use jaeger.ContribSpanObserver instead.
type SpanObserver interface {
OnSetOperationName(operationName string)
OnSetTag(key string, value interface{})
OnFinish(options opentracing.FinishOptions)
}
// compositeObserver is a dispatcher to other observers
type compositeObserver struct {
observers []ContribObserver
}
// compositeSpanObserver is a dispatcher to other span observers
type compositeSpanObserver struct {
observers []ContribSpanObserver
}
// noopSpanObserver is used when there are no observers registered
// on the Tracer or none of them returns span observers from OnStartSpan.
var noopSpanObserver = &compositeSpanObserver{}
func (o *compositeObserver) append(contribObserver ContribObserver) {
o.observers = append(o.observers, contribObserver)
}
func (o *compositeObserver) OnStartSpan(sp opentracing.Span, operationName string, options opentracing.StartSpanOptions) ContribSpanObserver {
var spanObservers []ContribSpanObserver
for _, obs := range o.observers {
spanObs, ok := obs.OnStartSpan(sp, operationName, options)
if ok {
if spanObservers == nil {
spanObservers = make([]ContribSpanObserver, 0, len(o.observers))
}
spanObservers = append(spanObservers, spanObs)
}
}
if len(spanObservers) == 0 {
return noopSpanObserver
}
return &compositeSpanObserver{observers: spanObservers}
}
func (o *compositeSpanObserver) OnSetOperationName(operationName string) {
for _, obs := range o.observers {
obs.OnSetOperationName(operationName)
}
}
func (o *compositeSpanObserver) OnSetTag(key string, value interface{}) {
for _, obs := range o.observers {
obs.OnSetTag(key, value)
}
}
func (o *compositeSpanObserver) OnFinish(options opentracing.FinishOptions) {
for _, obs := range o.observers {
obs.OnFinish(options)
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"testing"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/stretchr/testify/assert"
)
func TestEmptyObserver(t *testing.T) {
tracer, closer := NewTracer("test", NewConstSampler(true), NewInMemoryReporter())
defer closer.Close()
s := tracer.StartSpan("test", ext.RPCServerOption(nil))
s.Finish()
assert.Equal(t, s.(*Span).observer, noopSpanObserver)
}
func TestObservers(t *testing.T) {
tracer, closer := NewTracer(
"test",
NewConstSampler(true),
NewInMemoryReporter(),
TracerOptions.Observer(testObserver{}),
TracerOptions.Observer(testObserver{}),
)
defer closer.Close()
s := tracer.StartSpan("test", ext.RPCServerOption(nil))
forEachObs := func(f func(so *testSpanObserver)) {
observers := s.(*Span).observer.(*compositeSpanObserver).observers
assert.Len(t, observers, 2)
for _, so := range observers {
f(so.(*testSpanObserver))
}
}
forEachObs(func(so *testSpanObserver) {
assert.Equal(t, testSpanObserver{
operationName: "test",
tags: map[string]interface{}{
"span.kind": ext.SpanKindRPCServerEnum,
},
}, *so)
})
s.SetOperationName("test2")
s.SetTag("bender", "rodriguez")
forEachObs(func(so *testSpanObserver) {
assert.Equal(t, testSpanObserver{
operationName: "test2",
tags: map[string]interface{}{
"span.kind": ext.SpanKindRPCServerEnum,
"bender": "rodriguez",
},
}, *so)
})
s.Finish()
forEachObs(func(so *testSpanObserver) {
assert.True(t, so.finished)
})
}
type testObserver struct{}
type testSpanObserver struct {
operationName string
tags map[string]interface{}
finished bool
}
func (o testObserver) OnStartSpan(operationName string, options opentracing.StartSpanOptions) SpanObserver {
tags := make(map[string]interface{})
for k, v := range options.Tags {
tags[k] = v
}
return &testSpanObserver{
operationName: operationName,
tags: tags,
}
}
func (o *testSpanObserver) OnSetOperationName(operationName string) {
o.operationName = operationName
}
func (o *testSpanObserver) OnSetTag(key string, value interface{}) {
o.tags[key] = value
}
func (o *testSpanObserver) OnFinish(options opentracing.FinishOptions) {
o.finished = true
}

View File

@ -0,0 +1,300 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"net/url"
"strings"
"sync"
opentracing "github.com/opentracing/opentracing-go"
)
// Injector is responsible for injecting SpanContext instances in a manner suitable
// for propagation via a format-specific "carrier" object. Typically the
// injection will take place across an RPC boundary, but message queues and
// other IPC mechanisms are also reasonable places to use an Injector.
type Injector interface {
// Inject takes `SpanContext` and injects it into `carrier`. The actual type
// of `carrier` depends on the `format` passed to `Tracer.Inject()`.
//
// Implementations may return opentracing.ErrInvalidCarrier or any other
// implementation-specific error if injection fails.
Inject(ctx SpanContext, carrier interface{}) error
}
// Extractor is responsible for extracting SpanContext instances from a
// format-specific "carrier" object. Typically the extraction will take place
// on the server side of an RPC boundary, but message queues and other IPC
// mechanisms are also reasonable places to use an Extractor.
type Extractor interface {
// Extract decodes a SpanContext instance from the given `carrier`,
// or (nil, opentracing.ErrSpanContextNotFound) if no context could
// be found in the `carrier`.
Extract(carrier interface{}) (SpanContext, error)
}
type textMapPropagator struct {
headerKeys *HeadersConfig
metrics Metrics
encodeValue func(string) string
decodeValue func(string) string
}
func newTextMapPropagator(headerKeys *HeadersConfig, metrics Metrics) *textMapPropagator {
return &textMapPropagator{
headerKeys: headerKeys,
metrics: metrics,
encodeValue: func(val string) string {
return val
},
decodeValue: func(val string) string {
return val
},
}
}
func newHTTPHeaderPropagator(headerKeys *HeadersConfig, metrics Metrics) *textMapPropagator {
return &textMapPropagator{
headerKeys: headerKeys,
metrics: metrics,
encodeValue: func(val string) string {
return url.QueryEscape(val)
},
decodeValue: func(val string) string {
// ignore decoding errors, cannot do anything about them
if v, err := url.QueryUnescape(val); err == nil {
return v
}
return val
},
}
}
type binaryPropagator struct {
tracer *Tracer
buffers sync.Pool
}
func newBinaryPropagator(tracer *Tracer) *binaryPropagator {
return &binaryPropagator{
tracer: tracer,
buffers: sync.Pool{New: func() interface{} { return &bytes.Buffer{} }},
}
}
func (p *textMapPropagator) Inject(
sc SpanContext,
abstractCarrier interface{},
) error {
textMapWriter, ok := abstractCarrier.(opentracing.TextMapWriter)
if !ok {
return opentracing.ErrInvalidCarrier
}
// Do not encode the string with trace context to avoid accidental double-encoding
// if people are using opentracing < 0.10.0. Our colon-separated representation
// of the trace context is already safe for HTTP headers.
textMapWriter.Set(p.headerKeys.TraceContextHeaderName, sc.String())
for k, v := range sc.baggage {
safeKey := p.addBaggageKeyPrefix(k)
safeVal := p.encodeValue(v)
textMapWriter.Set(safeKey, safeVal)
}
return nil
}
func (p *textMapPropagator) Extract(abstractCarrier interface{}) (SpanContext, error) {
textMapReader, ok := abstractCarrier.(opentracing.TextMapReader)
if !ok {
return emptyContext, opentracing.ErrInvalidCarrier
}
var ctx SpanContext
var baggage map[string]string
err := textMapReader.ForeachKey(func(rawKey, value string) error {
key := strings.ToLower(rawKey) // TODO not necessary for plain TextMap
if key == p.headerKeys.TraceContextHeaderName {
var err error
safeVal := p.decodeValue(value)
if ctx, err = ContextFromString(safeVal); err != nil {
return err
}
} else if key == p.headerKeys.JaegerDebugHeader {
ctx.debugID = p.decodeValue(value)
} else if key == p.headerKeys.JaegerBaggageHeader {
if baggage == nil {
baggage = make(map[string]string)
}
for k, v := range p.parseCommaSeparatedMap(value) {
baggage[k] = v
}
} else if strings.HasPrefix(key, p.headerKeys.TraceBaggageHeaderPrefix) {
if baggage == nil {
baggage = make(map[string]string)
}
safeKey := p.removeBaggageKeyPrefix(key)
safeVal := p.decodeValue(value)
baggage[safeKey] = safeVal
}
return nil
})
if err != nil {
p.metrics.DecodingErrors.Inc(1)
return emptyContext, err
}
if !ctx.traceID.IsValid() && ctx.debugID == "" && len(baggage) == 0 {
return emptyContext, opentracing.ErrSpanContextNotFound
}
ctx.baggage = baggage
return ctx, nil
}
func (p *binaryPropagator) Inject(
sc SpanContext,
abstractCarrier interface{},
) error {
carrier, ok := abstractCarrier.(io.Writer)
if !ok {
return opentracing.ErrInvalidCarrier
}
// Handle the tracer context
if err := binary.Write(carrier, binary.BigEndian, sc.traceID); err != nil {
return err
}
if err := binary.Write(carrier, binary.BigEndian, sc.spanID); err != nil {
return err
}
if err := binary.Write(carrier, binary.BigEndian, sc.parentID); err != nil {
return err
}
if err := binary.Write(carrier, binary.BigEndian, sc.flags); err != nil {
return err
}
// Handle the baggage items
if err := binary.Write(carrier, binary.BigEndian, int32(len(sc.baggage))); err != nil {
return err
}
for k, v := range sc.baggage {
if err := binary.Write(carrier, binary.BigEndian, int32(len(k))); err != nil {
return err
}
io.WriteString(carrier, k)
if err := binary.Write(carrier, binary.BigEndian, int32(len(v))); err != nil {
return err
}
io.WriteString(carrier, v)
}
return nil
}
func (p *binaryPropagator) Extract(abstractCarrier interface{}) (SpanContext, error) {
carrier, ok := abstractCarrier.(io.Reader)
if !ok {
return emptyContext, opentracing.ErrInvalidCarrier
}
var ctx SpanContext
if err := binary.Read(carrier, binary.BigEndian, &ctx.traceID); err != nil {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
if err := binary.Read(carrier, binary.BigEndian, &ctx.spanID); err != nil {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
if err := binary.Read(carrier, binary.BigEndian, &ctx.parentID); err != nil {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
if err := binary.Read(carrier, binary.BigEndian, &ctx.flags); err != nil {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
// Handle the baggage items
var numBaggage int32
if err := binary.Read(carrier, binary.BigEndian, &numBaggage); err != nil {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
if iNumBaggage := int(numBaggage); iNumBaggage > 0 {
ctx.baggage = make(map[string]string, iNumBaggage)
buf := p.buffers.Get().(*bytes.Buffer)
defer p.buffers.Put(buf)
var keyLen, valLen int32
for i := 0; i < iNumBaggage; i++ {
if err := binary.Read(carrier, binary.BigEndian, &keyLen); err != nil {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
buf.Reset()
buf.Grow(int(keyLen))
if n, err := io.CopyN(buf, carrier, int64(keyLen)); err != nil || int32(n) != keyLen {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
key := buf.String()
if err := binary.Read(carrier, binary.BigEndian, &valLen); err != nil {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
buf.Reset()
buf.Grow(int(valLen))
if n, err := io.CopyN(buf, carrier, int64(valLen)); err != nil || int32(n) != valLen {
return emptyContext, opentracing.ErrSpanContextCorrupted
}
ctx.baggage[key] = buf.String()
}
}
return ctx, nil
}
// Converts a comma separated key value pair list into a map
// e.g. key1=value1, key2=value2, key3 = value3
// is converted to map[string]string { "key1" : "value1",
// "key2" : "value2",
// "key3" : "value3" }
func (p *textMapPropagator) parseCommaSeparatedMap(value string) map[string]string {
baggage := make(map[string]string)
value, err := url.QueryUnescape(value)
if err != nil {
log.Printf("Unable to unescape %s, %v", value, err)
return baggage
}
for _, kvpair := range strings.Split(value, ",") {
kv := strings.Split(strings.TrimSpace(kvpair), "=")
if len(kv) == 2 {
baggage[kv[0]] = kv[1]
} else {
log.Printf("Malformed value passed in for %s", p.headerKeys.JaegerBaggageHeader)
}
}
return baggage
}
// Converts a baggage item key into an http header format,
// by prepending TraceBaggageHeaderPrefix and encoding the key string
func (p *textMapPropagator) addBaggageKeyPrefix(key string) string {
// TODO encodeBaggageKeyAsHeader add caching and escaping
return fmt.Sprintf("%v%v", p.headerKeys.TraceBaggageHeaderPrefix, key)
}
func (p *textMapPropagator) removeBaggageKeyPrefix(key string) string {
// TODO decodeBaggageHeaderKey add caching and escaping
return key[len(p.headerKeys.TraceBaggageHeaderPrefix):]
}

View File

@ -0,0 +1,268 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"bytes"
"net/http"
"testing"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-lib/metrics/testutils"
)
func initMetrics() (*metrics.LocalFactory, *Metrics) {
factory := metrics.NewLocalFactory(0)
return factory, NewMetrics(factory, nil)
}
func TestSpanPropagator(t *testing.T) {
const op = "test"
reporter := NewInMemoryReporter()
metricsFactory, metrics := initMetrics()
tracer, closer := NewTracer("x", NewConstSampler(true), reporter, TracerOptions.Metrics(metrics), TracerOptions.ZipkinSharedRPCSpan(true))
mapc := opentracing.TextMapCarrier(make(map[string]string))
httpc := opentracing.HTTPHeadersCarrier(http.Header{})
tests := []struct {
format, carrier, formatName interface{}
}{
{SpanContextFormat, new(SpanContext), "TraceContextFormat"},
{opentracing.Binary, new(bytes.Buffer), "Binary"},
{opentracing.TextMap, mapc, "TextMap"},
{opentracing.HTTPHeaders, httpc, "HTTPHeaders"},
}
sp := tracer.StartSpan(op)
sp.SetTag("x", "y") // to avoid later comparing nil vs. []
sp.SetBaggageItem("foo", "bar")
for _, test := range tests {
// starting normal child to extract its serialized context
child := tracer.StartSpan(op, opentracing.ChildOf(sp.Context()))
err := tracer.Inject(child.Context(), test.format, test.carrier)
assert.NoError(t, err)
// Note: we're not finishing the above span
childCtx, err := tracer.Extract(test.format, test.carrier)
assert.NoError(t, err)
child = tracer.StartSpan(test.formatName.(string), ext.RPCServerOption(childCtx))
child.SetTag("x", "y") // to avoid later comparing nil vs. []
child.Finish()
}
sp.Finish()
closer.Close()
otSpans := reporter.GetSpans()
require.Equal(t, len(tests)+1, len(otSpans), "unexpected number of spans reporter")
spans := make([]*Span, len(otSpans))
for i, s := range otSpans {
spans[i] = s.(*Span)
}
// The last span is the original one.
exp, spans := spans[len(spans)-1], spans[:len(spans)-1]
exp.duration = time.Duration(123)
exp.startTime = time.Time{}.Add(1)
require.Len(t, exp.logs, 1) // The parent span should have baggage logs
fields := exp.logs[0].Fields
require.Len(t, fields, 3)
require.Equal(t, "event", fields[0].Key())
require.Equal(t, "baggage", fields[0].Value().(string))
require.Equal(t, "key", fields[1].Key())
require.Equal(t, "foo", fields[1].Value().(string))
require.Equal(t, "value", fields[2].Key())
require.Equal(t, "bar", fields[2].Value().(string))
if exp.context.ParentID() != 0 {
t.Fatalf("Root span's ParentID %d is not 0", exp.context.ParentID())
}
expTags := exp.tags[2:] // skip two sampler.xxx tags
for i, sp := range spans {
formatName := sp.operationName
if a, e := sp.context.ParentID(), exp.context.SpanID(); a != e {
t.Fatalf("%d: ParentID %d does not match expectation %d", i, a, e)
} else {
// Prepare for comparison.
sp.context.spanID, sp.context.parentID = exp.context.SpanID(), 0
sp.duration, sp.startTime = exp.duration, exp.startTime
}
assert.Equal(t, exp.context, sp.context, formatName)
assert.Equal(t, "span.kind", sp.tags[0].key)
assert.Equal(t, expTags, sp.tags[1:] /*skip span.kind tag*/, formatName)
assert.Empty(t, sp.logs, formatName)
// Override collections to avoid tripping comparison on different pointers
sp.context = exp.context
sp.tags = exp.tags
sp.logs = exp.logs
sp.operationName = op
sp.references = exp.references
// Compare the rest of the fields
assert.Equal(t, exp, sp, formatName)
}
testutils.AssertCounterMetrics(t, metricsFactory, []testutils.ExpectedMetric{
{Name: "jaeger.spans", Tags: map[string]string{"group": "sampling", "sampled": "y"}, Value: 1 + 2*len(tests)},
{Name: "jaeger.spans", Tags: map[string]string{"group": "lifecycle", "state": "started"}, Value: 1 + 2*len(tests)},
{Name: "jaeger.spans", Tags: map[string]string{"group": "lifecycle", "state": "finished"}, Value: 1 + len(tests)},
{Name: "jaeger.traces", Tags: map[string]string{"state": "started", "sampled": "y"}, Value: 1},
{Name: "jaeger.traces", Tags: map[string]string{"state": "joined", "sampled": "y"}, Value: len(tests)},
}...)
}
func TestSpanIntegrityAfterSerialize(t *testing.T) {
serializedString := "f6c385a2c57ed8d7:b04a90b7723bdc:76c385a2c57ed8d7:1"
context, err := ContextFromString(serializedString)
require.NoError(t, err)
require.True(t, context.traceID.Low > (uint64(1)<<63))
require.True(t, int64(context.traceID.Low) < 0)
newSerializedString := context.String()
require.Equal(t, serializedString, newSerializedString)
}
func TestDecodingError(t *testing.T) {
reporter := NewInMemoryReporter()
metricsFactory, metrics := initMetrics()
tracer, closer := NewTracer("x", NewConstSampler(true), reporter, TracerOptions.Metrics(metrics))
defer closer.Close()
badHeader := "x.x.x.x"
httpHeader := http.Header{}
httpHeader.Add(TraceContextHeaderName, badHeader)
tmc := opentracing.HTTPHeadersCarrier(httpHeader)
_, err := tracer.Extract(opentracing.HTTPHeaders, tmc)
assert.Error(t, err)
testutils.AssertCounterMetrics(t, metricsFactory, testutils.ExpectedMetric{Name: "jaeger.decoding-errors", Value: 1})
}
func TestBaggagePropagationHTTP(t *testing.T) {
tracer, closer := NewTracer("DOOP", NewConstSampler(true), NewNullReporter())
defer closer.Close()
sp1 := tracer.StartSpan("s1").(*Span)
sp1.SetBaggageItem("Some_Key", "12345")
assert.Equal(t, "12345", sp1.BaggageItem("Some_Key"), "baggage: %+v", sp1.context.baggage)
assert.Empty(t, sp1.BaggageItem("some-KEY"), "baggage: %+v", sp1.context.baggage)
sp1.SetBaggageItem("Some_Key", "98:765")
assert.Equal(t, "98:765", sp1.BaggageItem("Some_Key"), "baggage: %+v", sp1.context.baggage)
assert.Empty(t, sp1.BaggageItem("some-KEY"), "baggage: %+v", sp1.context.baggage)
h := http.Header{}
h.Add("header1", "value1") // make sure this does not get unmarshalled as baggage
err := tracer.Inject(sp1.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(h))
require.NoError(t, err)
// check that colon : was encoded as %3A
assert.Equal(t, "98%3A765", h.Get(TraceBaggageHeaderPrefix+"Some_Key"), "headers: %+v", h)
sp2, err := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(h))
require.NoError(t, err)
assert.Equal(t, map[string]string{"some_key": "98:765"}, sp2.(SpanContext).baggage)
}
func TestJaegerBaggageHeader(t *testing.T) {
metricsFactory, metrics := initMetrics()
tracer, closer := NewTracer("DOOP",
NewConstSampler(true),
NewNullReporter(),
TracerOptions.Metrics(metrics),
)
defer closer.Close()
h := http.Header{}
h.Add(JaegerBaggageHeader, "key1=value1, key 2=value two")
ctx, err := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(h))
require.NoError(t, err)
sp := tracer.StartSpan("root", opentracing.ChildOf(ctx)).(*Span)
assert.Equal(t, "value1", sp.BaggageItem("key1"))
assert.Equal(t, "value two", sp.BaggageItem("key 2"))
// ensure that traces.started counter is incremented, not traces.joined
testutils.AssertCounterMetrics(t, metricsFactory,
testutils.ExpectedMetric{
Name: "jaeger.traces", Tags: map[string]string{"state": "started", "sampled": "y"}, Value: 1,
},
)
}
func TestParseCommaSeperatedMap(t *testing.T) {
var testcases = []struct {
in string
out map[string]string
}{
{"hobbit=Bilbo Baggins", map[string]string{"hobbit": "Bilbo Baggins"}},
{"hobbit=Bilbo Baggins, dwarf= Thrain", map[string]string{"hobbit": "Bilbo Baggins", "dwarf": " Thrain"}},
{"kevin spacey=actor", map[string]string{"kevin spacey": "actor"}},
{"kevin%20spacey=se7en%3Aactor", map[string]string{"kevin spacey": "se7en:actor"}},
{"key1=, key2=", map[string]string{"key1": "", "key2": ""}},
{"malformed", map[string]string{}},
{"malformed, string", map[string]string{}},
{"another malformed string", map[string]string{}},
}
for _, testcase := range testcases {
m := (&textMapPropagator{
headerKeys: getDefaultHeadersConfig(),
}).parseCommaSeparatedMap(testcase.in)
assert.Equal(t, testcase.out, m)
}
}
func TestDebugCorrelationID(t *testing.T) {
metricsFactory, metrics := initMetrics()
tracer, closer := NewTracer("DOOP",
NewConstSampler(true),
NewNullReporter(),
TracerOptions.Metrics(metrics),
)
defer closer.Close()
h := http.Header{}
h.Add(JaegerDebugHeader, "value1")
ctx, err := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(h))
require.NoError(t, err)
assert.EqualValues(t, 0, ctx.(SpanContext).parentID)
assert.EqualValues(t, "value1", ctx.(SpanContext).debugID)
sp := tracer.StartSpan("root", opentracing.ChildOf(ctx)).(*Span)
assert.EqualValues(t, 0, sp.context.parentID)
assert.True(t, sp.context.traceID.IsValid())
assert.True(t, sp.context.IsSampled())
assert.True(t, sp.context.IsDebug())
tagFound := false
for _, tag := range sp.tags {
if tag.key == JaegerDebugHeader {
assert.Equal(t, "value1", tag.value)
tagFound = true
}
}
assert.True(t, tagFound)
// ensure that traces.started counter is incremented, not traces.joined
testutils.AssertCounterMetrics(t, metricsFactory,
testutils.ExpectedMetric{
Name: "jaeger.traces", Tags: map[string]string{"state": "started", "sampled": "y"}, Value: 1,
},
)
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import "github.com/opentracing/opentracing-go"
// Reference represents a causal reference to other Spans (via their SpanContext).
type Reference struct {
Type opentracing.SpanReferenceType
Context SpanContext
}

View File

@ -0,0 +1,262 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go/log"
)
// Reporter is called by the tracer when a span is completed to report the span to the tracing collector.
type Reporter interface {
// Report submits a new span to collectors, possibly asynchronously and/or with buffering.
Report(span *Span)
// Close does a clean shutdown of the reporter, flushing any traces that may be buffered in memory.
Close()
}
// ------------------------------
type nullReporter struct{}
// NewNullReporter creates a no-op reporter that ignores all reported spans.
func NewNullReporter() Reporter {
return &nullReporter{}
}
// Report implements Report() method of Reporter by doing nothing.
func (r *nullReporter) Report(span *Span) {
// no-op
}
// Close implements Close() method of Reporter by doing nothing.
func (r *nullReporter) Close() {
// no-op
}
// ------------------------------
type loggingReporter struct {
logger Logger
}
// NewLoggingReporter creates a reporter that logs all reported spans to provided logger.
func NewLoggingReporter(logger Logger) Reporter {
return &loggingReporter{logger}
}
// Report implements Report() method of Reporter by logging the span to the logger.
func (r *loggingReporter) Report(span *Span) {
r.logger.Infof("Reporting span %+v", span)
}
// Close implements Close() method of Reporter by doing nothing.
func (r *loggingReporter) Close() {
// no-op
}
// ------------------------------
// InMemoryReporter is used for testing, and simply collects spans in memory.
type InMemoryReporter struct {
spans []opentracing.Span
lock sync.Mutex
}
// NewInMemoryReporter creates a reporter that stores spans in memory.
// NOTE: the Tracer should be created with options.PoolSpans = false.
func NewInMemoryReporter() *InMemoryReporter {
return &InMemoryReporter{
spans: make([]opentracing.Span, 0, 10),
}
}
// Report implements Report() method of Reporter by storing the span in the buffer.
func (r *InMemoryReporter) Report(span *Span) {
r.lock.Lock()
r.spans = append(r.spans, span)
r.lock.Unlock()
}
// Close implements Close() method of Reporter by doing nothing.
func (r *InMemoryReporter) Close() {
// no-op
}
// SpansSubmitted returns the number of spans accumulated in the buffer.
func (r *InMemoryReporter) SpansSubmitted() int {
r.lock.Lock()
defer r.lock.Unlock()
return len(r.spans)
}
// GetSpans returns accumulated spans as a copy of the buffer.
func (r *InMemoryReporter) GetSpans() []opentracing.Span {
r.lock.Lock()
defer r.lock.Unlock()
copied := make([]opentracing.Span, len(r.spans))
copy(copied, r.spans)
return copied
}
// Reset clears all accumulated spans.
func (r *InMemoryReporter) Reset() {
r.lock.Lock()
defer r.lock.Unlock()
r.spans = nil
}
// ------------------------------
type compositeReporter struct {
reporters []Reporter
}
// NewCompositeReporter creates a reporter that ignores all reported spans.
func NewCompositeReporter(reporters ...Reporter) Reporter {
return &compositeReporter{reporters: reporters}
}
// Report implements Report() method of Reporter by delegating to each underlying reporter.
func (r *compositeReporter) Report(span *Span) {
for _, reporter := range r.reporters {
reporter.Report(span)
}
}
// Close implements Close() method of Reporter by closing each underlying reporter.
func (r *compositeReporter) Close() {
for _, reporter := range r.reporters {
reporter.Close()
}
}
// ------------------------------
const (
defaultQueueSize = 100
defaultBufferFlushInterval = 10 * time.Second
)
type remoteReporter struct {
// must be first in the struct because `sync/atomic` expects 64-bit alignment.
// Cf. https://github.com/uber/jaeger-client-go/issues/155, https://goo.gl/zW7dgq
queueLength int64
reporterOptions
sender Transport
queue chan *Span
queueDrained sync.WaitGroup
flushSignal chan *sync.WaitGroup
}
// NewRemoteReporter creates a new reporter that sends spans out of process by means of Sender
func NewRemoteReporter(sender Transport, opts ...ReporterOption) Reporter {
options := reporterOptions{}
for _, option := range opts {
option(&options)
}
if options.bufferFlushInterval <= 0 {
options.bufferFlushInterval = defaultBufferFlushInterval
}
if options.logger == nil {
options.logger = log.NullLogger
}
if options.metrics == nil {
options.metrics = NewNullMetrics()
}
if options.queueSize <= 0 {
options.queueSize = defaultQueueSize
}
reporter := &remoteReporter{
reporterOptions: options,
sender: sender,
flushSignal: make(chan *sync.WaitGroup),
queue: make(chan *Span, options.queueSize),
}
go reporter.processQueue()
return reporter
}
// Report implements Report() method of Reporter.
// It passes the span to a background go-routine for submission to Jaeger.
func (r *remoteReporter) Report(span *Span) {
select {
case r.queue <- span:
atomic.AddInt64(&r.queueLength, 1)
default:
r.metrics.ReporterDropped.Inc(1)
}
}
// Close implements Close() method of Reporter by waiting for the queue to be drained.
func (r *remoteReporter) Close() {
r.queueDrained.Add(1)
close(r.queue)
r.queueDrained.Wait()
r.sender.Close()
}
// processQueue reads spans from the queue, converts them to Thrift, and stores them in an internal buffer.
// When the buffer length reaches batchSize, it is flushed by submitting the accumulated spans to Jaeger.
// Buffer also gets flushed automatically every batchFlushInterval seconds, just in case the tracer stopped
// reporting new spans.
func (r *remoteReporter) processQueue() {
timer := time.NewTicker(r.bufferFlushInterval)
for {
select {
case span, ok := <-r.queue:
if ok {
atomic.AddInt64(&r.queueLength, -1)
if flushed, err := r.sender.Append(span); err != nil {
r.metrics.ReporterFailure.Inc(int64(flushed))
r.logger.Error(fmt.Sprintf("error reporting span %q: %s", span.OperationName(), err.Error()))
} else if flushed > 0 {
r.metrics.ReporterSuccess.Inc(int64(flushed))
// to reduce the number of gauge stats, we only emit queue length on flush
r.metrics.ReporterQueueLength.Update(atomic.LoadInt64(&r.queueLength))
}
} else {
// queue closed
timer.Stop()
r.flush()
r.queueDrained.Done()
return
}
case <-timer.C:
r.flush()
case wg := <-r.flushSignal: // for testing
r.flush()
wg.Done()
}
}
}
// flush causes the Sender to flush its accumulated spans and clear the buffer
func (r *remoteReporter) flush() {
if flushed, err := r.sender.Flush(); err != nil {
r.metrics.ReporterFailure.Inc(int64(flushed))
r.logger.Error(err.Error())
} else if flushed > 0 {
r.metrics.ReporterSuccess.Inc(int64(flushed))
}
}

View File

@ -0,0 +1,69 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"time"
)
// ReporterOption is a function that sets some option on the reporter.
type ReporterOption func(c *reporterOptions)
// ReporterOptions is a factory for all available ReporterOption's
var ReporterOptions reporterOptions
// reporterOptions control behavior of the reporter.
type reporterOptions struct {
// queueSize is the size of internal queue where reported spans are stored before they are processed in the background
queueSize int
// bufferFlushInterval is how often the buffer is force-flushed, even if it's not full
bufferFlushInterval time.Duration
// logger is used to log errors of span submissions
logger Logger
// metrics is used to record runtime stats
metrics *Metrics
}
// QueueSize creates a ReporterOption that sets the size of the internal queue where
// spans are stored before they are processed.
func (reporterOptions) QueueSize(queueSize int) ReporterOption {
return func(r *reporterOptions) {
r.queueSize = queueSize
}
}
// Metrics creates a ReporterOption that initializes Metrics in the reporter,
// which is used to record runtime statistics.
func (reporterOptions) Metrics(metrics *Metrics) ReporterOption {
return func(r *reporterOptions) {
r.metrics = metrics
}
}
// BufferFlushInterval creates a ReporterOption that sets how often the queue
// is force-flushed.
func (reporterOptions) BufferFlushInterval(bufferFlushInterval time.Duration) ReporterOption {
return func(r *reporterOptions) {
r.bufferFlushInterval = bufferFlushInterval
}
}
// Logger creates a ReporterOption that initializes the logger used to log
// errors of span submissions.
func (reporterOptions) Logger(logger Logger) ReporterOption {
return func(r *reporterOptions) {
r.logger = logger
}
}

View File

@ -0,0 +1,272 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"io"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/uber/jaeger-lib/metrics"
mTestutils "github.com/uber/jaeger-lib/metrics/testutils"
"github.com/uber/jaeger-client-go/testutils"
j "github.com/uber/jaeger-client-go/thrift-gen/jaeger"
)
type reporterSuite struct {
suite.Suite
tracer opentracing.Tracer
closer io.Closer
serviceName string
reporter *remoteReporter
collector *fakeSender
metricsFactory *metrics.LocalFactory
}
func (s *reporterSuite) SetupTest() {
s.metricsFactory = metrics.NewLocalFactory(0)
metrics := NewMetrics(s.metricsFactory, nil)
s.serviceName = "DOOP"
s.collector = &fakeSender{}
s.reporter = NewRemoteReporter(
s.collector, ReporterOptions.Metrics(metrics),
).(*remoteReporter)
s.tracer, s.closer = NewTracer(
"reporter-test-service",
NewConstSampler(true),
s.reporter,
TracerOptions.Metrics(metrics))
s.NotNil(s.tracer)
}
func (s *reporterSuite) TearDownTest() {
s.closer.Close()
s.tracer = nil
s.reporter = nil
s.collector = nil
}
func TestReporter(t *testing.T) {
suite.Run(t, new(reporterSuite))
}
func (s *reporterSuite) flushReporter() {
// Wait for reporter queue to add spans to buffer. We could've called reporter.Close(),
// but then it fails when the test suite calls close on it again (via tracer's Closer).
time.Sleep(5 * time.Millisecond)
var wg sync.WaitGroup
wg.Add(1)
s.reporter.flushSignal <- &wg
wg.Wait()
}
func (s *reporterSuite) TestRootSpanTags() {
s.metricsFactory.Clear()
sp := s.tracer.StartSpan("get_name")
ext.SpanKindRPCServer.Set(sp)
ext.PeerService.Set(sp, s.serviceName)
sp.Finish()
s.flushReporter()
s.Equal(1, len(s.collector.Spans()))
span := s.collector.Spans()[0]
s.Len(span.tags, 4)
s.EqualValues("server", span.tags[2].value, "span.kind should be server")
mTestutils.AssertCounterMetrics(s.T(), s.metricsFactory,
mTestutils.ExpectedMetric{
Name: "jaeger.reporter-spans",
Tags: map[string]string{"state": "success"},
Value: 1,
},
)
}
func (s *reporterSuite) TestClientSpan() {
s.metricsFactory.Clear()
sp := s.tracer.StartSpan("get_name")
ext.SpanKindRPCServer.Set(sp)
ext.PeerService.Set(sp, s.serviceName)
sp2 := s.tracer.StartSpan("get_last_name", opentracing.ChildOf(sp.Context()))
ext.SpanKindRPCClient.Set(sp2)
ext.PeerService.Set(sp2, s.serviceName)
sp2.Finish()
sp.Finish()
s.flushReporter()
s.Equal(2, len(s.collector.Spans()))
span := s.collector.Spans()[0] // child span is reported first
s.EqualValues(span.context.spanID, sp2.(*Span).context.spanID)
s.Len(span.tags, 2)
s.EqualValues("client", span.tags[0].value, "span.kind should be client")
mTestutils.AssertCounterMetrics(s.T(), s.metricsFactory,
mTestutils.ExpectedMetric{
Name: "jaeger.reporter-spans",
Tags: map[string]string{"state": "success"},
Value: 2,
},
)
}
func (s *reporterSuite) TestTagsAndEvents() {
sp := s.tracer.StartSpan("get_name")
sp.LogEvent("hello")
sp.LogEvent(strings.Repeat("long event ", 30))
expected := []string{"long", "ping", "awake", "awake", "one", "two", "three", "bite me",
SamplerParamTagKey, SamplerTypeTagKey, "does not compute"}
sp.SetTag("long", strings.Repeat("x", 300))
sp.SetTag("ping", "pong")
sp.SetTag("awake", true)
sp.SetTag("awake", false)
sp.SetTag("one", 1)
sp.SetTag("two", int32(2))
sp.SetTag("three", int64(3))
sp.SetTag("bite me", []byte{1})
sp.SetTag("does not compute", sp) // should be converted to string
sp.Finish()
s.flushReporter()
s.Equal(1, len(s.collector.Spans()))
span := s.collector.Spans()[0]
s.Equal(2, len(span.logs), "expecting two logs")
s.Equal(len(expected), len(span.tags),
"expecting %d tags", len(expected))
tags := []string{}
for _, tag := range span.tags {
tags = append(tags, string(tag.key))
}
sort.Strings(expected)
sort.Strings(tags)
s.Equal(expected, tags, "expecting %d tags", len(expected))
s.NotNil(findDomainLog(span, "hello"), "expecting 'hello' log: %+v", span.logs)
}
func TestUDPReporter(t *testing.T) {
agent, err := testutils.StartMockAgent()
require.NoError(t, err)
defer agent.Close()
testRemoteReporter(t,
func(m *Metrics) (Transport, error) {
return NewUDPTransport(agent.SpanServerAddr(), 0)
},
func() []*j.Batch {
return agent.GetJaegerBatches()
})
}
func testRemoteReporter(
t *testing.T,
factory func(m *Metrics) (Transport, error),
getBatches func() []*j.Batch,
) {
metricsFactory := metrics.NewLocalFactory(0)
metrics := NewMetrics(metricsFactory, nil)
sender, err := factory(metrics)
require.NoError(t, err)
reporter := NewRemoteReporter(sender, ReporterOptions.Metrics(metrics)).(*remoteReporter)
tracer, closer := NewTracer(
"reporter-test-service",
NewConstSampler(true),
reporter,
TracerOptions.Metrics(metrics))
span := tracer.StartSpan("leela")
ext.SpanKindRPCClient.Set(span)
ext.PeerService.Set(span, "downstream")
span.Finish()
closer.Close() // close the tracer, which also closes and flushes the reporter
// however, in case of UDP reporter it's fire and forget, so we need to wait a bit
time.Sleep(5 * time.Millisecond)
batches := getBatches()
require.Equal(t, 1, len(batches))
require.Equal(t, 1, len(batches[0].Spans))
assert.Equal(t, "leela", batches[0].Spans[0].OperationName)
assert.Equal(t, "reporter-test-service", batches[0].Process.ServiceName)
tag := findJaegerTag("peer.service", batches[0].Spans[0].Tags)
assert.NotNil(t, tag)
assert.Equal(t, "downstream", *tag.VStr)
mTestutils.AssertCounterMetrics(t, metricsFactory, []mTestutils.ExpectedMetric{
{Name: "jaeger.reporter-spans", Tags: map[string]string{"state": "success"}, Value: 1},
{Name: "jaeger.reporter-spans", Tags: map[string]string{"state": "failure"}, Value: 0},
}...)
}
func (s *reporterSuite) TestMemoryReporterReport() {
sp := s.tracer.StartSpan("leela")
ext.PeerService.Set(sp, s.serviceName)
reporter := NewInMemoryReporter()
reporter.Report(sp.(*Span))
s.Equal(1, reporter.SpansSubmitted(), "expected number of spans submitted")
reporter.Close()
}
type fakeSender struct {
spans []*Span
mutex sync.Mutex
}
func (s *fakeSender) Append(span *Span) (int, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.spans = append(s.spans, span)
return 1, nil
}
func (s *fakeSender) Flush() (int, error) {
return 0, nil
}
func (s *fakeSender) Close() error { return nil }
func (s *fakeSender) Spans() []*Span {
s.mutex.Lock()
defer s.mutex.Unlock()
res := make([]*Span, len(s.spans))
copy(res, s.spans)
return res
}
func findDomainLog(span *Span, key string) *opentracing.LogRecord {
for _, log := range span.logs {
if log.Fields[0].Value().(string) == key {
return &log
}
}
return nil
}
func findDomainTag(span *Span, key string) *Tag {
for _, tag := range span.tags {
if tag.key == key {
return &tag
}
}
return nil
}

View File

@ -0,0 +1,5 @@
An Observer that can be used to emit RPC metrics
================================================
It can be attached to the tracer during tracer construction.
See `ExampleObserver` function in [observer_test.go](./observer_test.go).

View File

@ -0,0 +1,16 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package rpcmetrics implements an Observer that can be used to emit RPC metrics.
package rpcmetrics

View File

@ -0,0 +1,63 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
import "sync"
// normalizedEndpoints is a cache for endpointName -> safeName mappings.
type normalizedEndpoints struct {
names map[string]string
maxSize int
defaultName string
normalizer NameNormalizer
mux sync.RWMutex
}
func newNormalizedEndpoints(maxSize int, normalizer NameNormalizer) *normalizedEndpoints {
return &normalizedEndpoints{
maxSize: maxSize,
normalizer: normalizer,
names: make(map[string]string, maxSize),
}
}
// normalize looks up the name in the cache, if not found it uses normalizer
// to convert the name to a safe name. If called with more than maxSize unique
// names it returns "" for all other names beyond those already cached.
func (n *normalizedEndpoints) normalize(name string) string {
n.mux.RLock()
norm, ok := n.names[name]
l := len(n.names)
n.mux.RUnlock()
if ok {
return norm
}
if l >= n.maxSize {
return ""
}
return n.normalizeWithLock(name)
}
func (n *normalizedEndpoints) normalizeWithLock(name string) string {
norm := n.normalizer.Normalize(name)
n.mux.Lock()
defer n.mux.Unlock()
// cache may have grown while we were not holding the lock
if len(n.names) >= n.maxSize {
return ""
}
n.names[name] = norm
return norm
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNormalizedEndpoints(t *testing.T) {
n := newNormalizedEndpoints(1, DefaultNameNormalizer)
assertLen := func(l int) {
n.mux.RLock()
defer n.mux.RUnlock()
assert.Len(t, n.names, l)
}
assert.Equal(t, "ab-cd", n.normalize("ab^cd"), "one translation")
assert.Equal(t, "ab-cd", n.normalize("ab^cd"), "cache hit")
assertLen(1)
assert.Equal(t, "", n.normalize("xys"), "cache overflow")
assertLen(1)
}
func TestNormalizedEndpointsDoubleLocking(t *testing.T) {
n := newNormalizedEndpoints(1, DefaultNameNormalizer)
assert.Equal(t, "ab-cd", n.normalize("ab^cd"), "fill out the cache")
assert.Equal(t, "", n.normalizeWithLock("xys"), "cache overflow")
}

View File

@ -0,0 +1,124 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
import (
"sync"
"github.com/uber/jaeger-lib/metrics"
)
const (
otherEndpointsPlaceholder = "other"
endpointNameMetricTag = "endpoint"
)
// Metrics is a collection of metrics for an endpoint describing
// throughput, success, errors, and performance.
type Metrics struct {
// RequestCountSuccess is a counter of the total number of successes.
RequestCountSuccess metrics.Counter `metric:"requests" tags:"error=false"`
// RequestCountFailures is a counter of the number of times any failure has been observed.
RequestCountFailures metrics.Counter `metric:"requests" tags:"error=true"`
// RequestLatencySuccess is a latency histogram of succesful requests.
RequestLatencySuccess metrics.Timer `metric:"request_latency" tags:"error=false"`
// RequestLatencyFailures is a latency histogram of failed requests.
RequestLatencyFailures metrics.Timer `metric:"request_latency" tags:"error=true"`
// HTTPStatusCode2xx is a counter of the total number of requests with HTTP status code 200-299
HTTPStatusCode2xx metrics.Counter `metric:"http_requests" tags:"status_code=2xx"`
// HTTPStatusCode3xx is a counter of the total number of requests with HTTP status code 300-399
HTTPStatusCode3xx metrics.Counter `metric:"http_requests" tags:"status_code=3xx"`
// HTTPStatusCode4xx is a counter of the total number of requests with HTTP status code 400-499
HTTPStatusCode4xx metrics.Counter `metric:"http_requests" tags:"status_code=4xx"`
// HTTPStatusCode5xx is a counter of the total number of requests with HTTP status code 500-599
HTTPStatusCode5xx metrics.Counter `metric:"http_requests" tags:"status_code=5xx"`
}
func (m *Metrics) recordHTTPStatusCode(statusCode uint16) {
if statusCode >= 200 && statusCode < 300 {
m.HTTPStatusCode2xx.Inc(1)
} else if statusCode >= 300 && statusCode < 400 {
m.HTTPStatusCode3xx.Inc(1)
} else if statusCode >= 400 && statusCode < 500 {
m.HTTPStatusCode4xx.Inc(1)
} else if statusCode >= 500 && statusCode < 600 {
m.HTTPStatusCode5xx.Inc(1)
}
}
// MetricsByEndpoint is a registry/cache of metrics for each unique endpoint name.
// Only maxNumberOfEndpoints Metrics are stored, all other endpoint names are mapped
// to a generic endpoint name "other".
type MetricsByEndpoint struct {
metricsFactory metrics.Factory
endpoints *normalizedEndpoints
metricsByEndpoint map[string]*Metrics
mux sync.RWMutex
}
func newMetricsByEndpoint(
metricsFactory metrics.Factory,
normalizer NameNormalizer,
maxNumberOfEndpoints int,
) *MetricsByEndpoint {
return &MetricsByEndpoint{
metricsFactory: metricsFactory,
endpoints: newNormalizedEndpoints(maxNumberOfEndpoints, normalizer),
metricsByEndpoint: make(map[string]*Metrics, maxNumberOfEndpoints+1), // +1 for "other"
}
}
func (m *MetricsByEndpoint) get(endpoint string) *Metrics {
safeName := m.endpoints.normalize(endpoint)
if safeName == "" {
safeName = otherEndpointsPlaceholder
}
m.mux.RLock()
met := m.metricsByEndpoint[safeName]
m.mux.RUnlock()
if met != nil {
return met
}
return m.getWithWriteLock(safeName)
}
// split to make easier to test
func (m *MetricsByEndpoint) getWithWriteLock(safeName string) *Metrics {
m.mux.Lock()
defer m.mux.Unlock()
// it is possible that the name has been already registered after we released
// the read lock and before we grabbed the write lock, so check for that.
if met, ok := m.metricsByEndpoint[safeName]; ok {
return met
}
// it would be nice to create the struct before locking, since Init() is somewhat
// expensive, however some metrics backends (e.g. expvar) may not like duplicate metrics.
met := &Metrics{}
tags := map[string]string{endpointNameMetricTag: safeName}
metrics.Init(met, m.metricsFactory, tags)
m.metricsByEndpoint[safeName] = met
return met
}

View File

@ -0,0 +1,61 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/uber/jaeger-lib/metrics"
"github.com/uber/jaeger-lib/metrics/testutils"
)
// E.g. tags("key", "value", "key", "value")
func tags(kv ...string) map[string]string {
m := make(map[string]string)
for i := 0; i < len(kv)-1; i += 2 {
m[kv[i]] = kv[i+1]
}
return m
}
func endpointTags(endpoint string, kv ...string) map[string]string {
return tags(append([]string{"endpoint", endpoint}, kv...)...)
}
func TestMetricsByEndpoint(t *testing.T) {
met := metrics.NewLocalFactory(0)
mbe := newMetricsByEndpoint(met, DefaultNameNormalizer, 2)
m1 := mbe.get("abc1")
m2 := mbe.get("abc1") // from cache
m2a := mbe.getWithWriteLock("abc1") // from cache in double-checked lock
assert.Equal(t, m1, m2)
assert.Equal(t, m1, m2a)
m3 := mbe.get("abc3")
m4 := mbe.get("overflow")
m5 := mbe.get("overflow2")
for _, m := range []*Metrics{m1, m2, m2a, m3, m4, m5} {
m.RequestCountSuccess.Inc(1)
}
testutils.AssertCounterMetrics(t, met,
testutils.ExpectedMetric{Name: "requests", Tags: endpointTags("abc1", "error", "false"), Value: 3},
testutils.ExpectedMetric{Name: "requests", Tags: endpointTags("abc3", "error", "false"), Value: 1},
testutils.ExpectedMetric{Name: "requests", Tags: endpointTags("other", "error", "false"), Value: 2},
)
}

View File

@ -0,0 +1,101 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
// NameNormalizer is used to convert the endpoint names to strings
// that can be safely used as tags in the metrics.
type NameNormalizer interface {
Normalize(name string) string
}
// DefaultNameNormalizer converts endpoint names so that they contain only characters
// from the safe charset [a-zA-Z0-9-./_]. All other characters are replaced with '-'.
var DefaultNameNormalizer = &SimpleNameNormalizer{
SafeSets: []SafeCharacterSet{
&Range{From: 'a', To: 'z'},
&Range{From: 'A', To: 'Z'},
&Range{From: '0', To: '9'},
&Char{'-'},
&Char{'_'},
&Char{'/'},
&Char{'.'},
},
Replacement: '-',
}
// SimpleNameNormalizer uses a set of safe character sets.
type SimpleNameNormalizer struct {
SafeSets []SafeCharacterSet
Replacement byte
}
// SafeCharacterSet determines if the given character is "safe"
type SafeCharacterSet interface {
IsSafe(c byte) bool
}
// Range implements SafeCharacterSet
type Range struct {
From, To byte
}
// IsSafe implements SafeCharacterSet
func (r *Range) IsSafe(c byte) bool {
return c >= r.From && c <= r.To
}
// Char implements SafeCharacterSet
type Char struct {
Val byte
}
// IsSafe implements SafeCharacterSet
func (ch *Char) IsSafe(c byte) bool {
return c == ch.Val
}
// Normalize checks each character in the string against SafeSets,
// and if it's not safe substitutes it with Replacement.
func (n *SimpleNameNormalizer) Normalize(name string) string {
var retMe []byte
nameBytes := []byte(name)
for i, b := range nameBytes {
if n.safeByte(b) {
if retMe != nil {
retMe[i] = b
}
} else {
if retMe == nil {
retMe = make([]byte, len(nameBytes))
copy(retMe[0:i], nameBytes[0:i])
}
retMe[i] = n.Replacement
}
}
if retMe == nil {
return name
}
return string(retMe)
}
// safeByte checks if b against all safe charsets.
func (n *SimpleNameNormalizer) safeByte(b byte) bool {
for i := range n.SafeSets {
if n.SafeSets[i].IsSafe(b) {
return true
}
}
return false
}

View File

@ -0,0 +1,34 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSimpleNameNormalizer(t *testing.T) {
n := &SimpleNameNormalizer{
SafeSets: []SafeCharacterSet{
&Range{From: 'a', To: 'z'},
&Char{'-'},
},
Replacement: '-',
}
assert.Equal(t, "ab-cd", n.Normalize("ab-cd"), "all valid")
assert.Equal(t, "ab-cd", n.Normalize("ab.cd"), "single mismatch")
assert.Equal(t, "a--cd", n.Normalize("aB-cd"), "range letter mismatch")
}

View File

@ -0,0 +1,171 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
import (
"strconv"
"sync"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/uber/jaeger-lib/metrics"
jaeger "github.com/uber/jaeger-client-go"
)
const defaultMaxNumberOfEndpoints = 200
// Observer is an observer that can emit RPC metrics.
type Observer struct {
metricsByEndpoint *MetricsByEndpoint
}
// NewObserver creates a new observer that can emit RPC metrics.
func NewObserver(metricsFactory metrics.Factory, normalizer NameNormalizer) *Observer {
return &Observer{
metricsByEndpoint: newMetricsByEndpoint(
metricsFactory,
normalizer,
defaultMaxNumberOfEndpoints,
),
}
}
// OnStartSpan creates a new Observer for the span.
func (o *Observer) OnStartSpan(
operationName string,
options opentracing.StartSpanOptions,
) jaeger.SpanObserver {
return NewSpanObserver(o.metricsByEndpoint, operationName, options)
}
// SpanKind identifies the span as inboud, outbound, or internal
type SpanKind int
const (
// Local span kind
Local SpanKind = iota
// Inbound span kind
Inbound
// Outbound span kind
Outbound
)
// SpanObserver collects RPC metrics
type SpanObserver struct {
metricsByEndpoint *MetricsByEndpoint
operationName string
startTime time.Time
mux sync.Mutex
kind SpanKind
httpStatusCode uint16
err bool
}
// NewSpanObserver creates a new SpanObserver that can emit RPC metrics.
func NewSpanObserver(
metricsByEndpoint *MetricsByEndpoint,
operationName string,
options opentracing.StartSpanOptions,
) *SpanObserver {
so := &SpanObserver{
metricsByEndpoint: metricsByEndpoint,
operationName: operationName,
startTime: options.StartTime,
}
for k, v := range options.Tags {
so.handleTagInLock(k, v)
}
return so
}
// handleTags watches for special tags
// - SpanKind
// - HttpStatusCode
// - Error
func (so *SpanObserver) handleTagInLock(key string, value interface{}) {
if key == string(ext.SpanKind) {
if v, ok := value.(ext.SpanKindEnum); ok {
value = string(v)
}
if v, ok := value.(string); ok {
if v == string(ext.SpanKindRPCClientEnum) {
so.kind = Outbound
} else if v == string(ext.SpanKindRPCServerEnum) {
so.kind = Inbound
}
}
return
}
if key == string(ext.HTTPStatusCode) {
if v, ok := value.(uint16); ok {
so.httpStatusCode = v
} else if v, ok := value.(int); ok {
so.httpStatusCode = uint16(v)
} else if v, ok := value.(string); ok {
if vv, err := strconv.Atoi(v); err == nil {
so.httpStatusCode = uint16(vv)
}
}
return
}
if key == string(ext.Error) {
if v, ok := value.(bool); ok {
so.err = v
} else if v, ok := value.(string); ok {
if vv, err := strconv.ParseBool(v); err == nil {
so.err = vv
}
}
return
}
}
// OnFinish emits the RPC metrics. It only has an effect when operation name
// is not blank, and the span kind is an RPC server.
func (so *SpanObserver) OnFinish(options opentracing.FinishOptions) {
so.mux.Lock()
defer so.mux.Unlock()
if so.operationName == "" || so.kind != Inbound {
return
}
mets := so.metricsByEndpoint.get(so.operationName)
latency := options.FinishTime.Sub(so.startTime)
if so.err {
mets.RequestCountFailures.Inc(1)
mets.RequestLatencyFailures.Record(latency)
} else {
mets.RequestCountSuccess.Inc(1)
mets.RequestLatencySuccess.Record(latency)
}
mets.recordHTTPStatusCode(so.httpStatusCode)
}
// OnSetOperationName records new operation name.
func (so *SpanObserver) OnSetOperationName(operationName string) {
so.mux.Lock()
so.operationName = operationName
so.mux.Unlock()
}
// OnSetTag implements SpanObserver
func (so *SpanObserver) OnSetTag(key string, value interface{}) {
so.mux.Lock()
so.handleTagInLock(key, value)
so.mux.Unlock()
}

View File

@ -0,0 +1,177 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpcmetrics
import (
"fmt"
"testing"
"time"
opentracing "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/uber/jaeger-lib/metrics"
u "github.com/uber/jaeger-lib/metrics/testutils"
"github.com/opentracing/opentracing-go/ext"
jaeger "github.com/uber/jaeger-client-go"
)
func ExampleObserver() {
metricsFactory := metrics.NewLocalFactory(0)
metricsObserver := NewObserver(
metricsFactory,
DefaultNameNormalizer,
)
tracer, closer := jaeger.NewTracer(
"serviceName",
jaeger.NewConstSampler(true),
jaeger.NewInMemoryReporter(),
jaeger.TracerOptions.Observer(metricsObserver),
)
defer closer.Close()
span := tracer.StartSpan("test", ext.SpanKindRPCServer)
span.Finish()
c, _ := metricsFactory.Snapshot()
fmt.Printf("requests (success): %d\n", c["requests|endpoint=test|error=false"])
fmt.Printf("requests (failure): %d\n", c["requests|endpoint=test|error=true"])
// Output:
// requests (success): 1
// requests (failure): 0
}
type testTracer struct {
metrics *metrics.LocalFactory
tracer opentracing.Tracer
}
func withTestTracer(runTest func(tt *testTracer)) {
sampler := jaeger.NewConstSampler(true)
reporter := jaeger.NewInMemoryReporter()
metrics := metrics.NewLocalFactory(time.Minute)
observer := NewObserver(metrics, DefaultNameNormalizer)
tracer, closer := jaeger.NewTracer(
"test",
sampler,
reporter,
jaeger.TracerOptions.Observer(observer))
defer closer.Close()
runTest(&testTracer{
metrics: metrics,
tracer: tracer,
})
}
func TestObserver(t *testing.T) {
withTestTracer(func(testTracer *testTracer) {
ts := time.Now()
finishOptions := opentracing.FinishOptions{
FinishTime: ts.Add(50 * time.Millisecond),
}
testCases := []struct {
name string
tag opentracing.Tag
opNameOverride string
err bool
}{
{name: "local-span", tag: opentracing.Tag{Key: "x", Value: "y"}},
{name: "get-user", tag: ext.SpanKindRPCServer},
{name: "get-user", tag: ext.SpanKindRPCServer, opNameOverride: "get-user-override"},
{name: "get-user", tag: ext.SpanKindRPCServer, err: true},
{name: "get-user-client", tag: ext.SpanKindRPCClient},
}
for _, testCase := range testCases {
span := testTracer.tracer.StartSpan(
testCase.name,
testCase.tag,
opentracing.StartTime(ts),
)
if testCase.opNameOverride != "" {
span.SetOperationName(testCase.opNameOverride)
}
if testCase.err {
ext.Error.Set(span, true)
}
span.FinishWithOptions(finishOptions)
}
u.AssertCounterMetrics(t,
testTracer.metrics,
u.ExpectedMetric{Name: "requests", Tags: endpointTags("local-span", "error", "false"), Value: 0},
u.ExpectedMetric{Name: "requests", Tags: endpointTags("get-user", "error", "false"), Value: 1},
u.ExpectedMetric{Name: "requests", Tags: endpointTags("get-user", "error", "true"), Value: 1},
u.ExpectedMetric{Name: "requests", Tags: endpointTags("get-user-override", "error", "false"), Value: 1},
u.ExpectedMetric{Name: "requests", Tags: endpointTags("get-user-client", "error", "false"), Value: 0},
)
// TODO something wrong with string generation, .P99 should not be appended to the tag
// as a result we cannot use u.AssertGaugeMetrics
_, g := testTracer.metrics.Snapshot()
assert.EqualValues(t, 51, g["request_latency|endpoint=get-user|error=false.P99"])
assert.EqualValues(t, 51, g["request_latency|endpoint=get-user|error=true.P99"])
})
}
func TestTags(t *testing.T) {
type tagTestCase struct {
key string
value interface{}
metrics []u.ExpectedMetric
}
testCases := []tagTestCase{
{key: "something", value: 42, metrics: []u.ExpectedMetric{
{Name: "requests", Value: 1, Tags: tags("error", "false")},
}},
{key: "error", value: true, metrics: []u.ExpectedMetric{
{Name: "requests", Value: 1, Tags: tags("error", "true")},
}},
{key: "error", value: "true", metrics: []u.ExpectedMetric{
{Name: "requests", Value: 1, Tags: tags("error", "true")},
}},
}
for i := 2; i <= 5; i++ {
values := []interface{}{
i * 100,
uint16(i * 100),
fmt.Sprintf("%d00", i),
}
for _, v := range values {
testCases = append(testCases, tagTestCase{
key: "http.status_code", value: v, metrics: []u.ExpectedMetric{
{Name: "http_requests", Value: 1, Tags: tags("status_code", fmt.Sprintf("%dxx", i))},
},
})
}
}
for _, tc := range testCases {
testCase := tc // capture loop var
for i := range testCase.metrics {
testCase.metrics[i].Tags["endpoint"] = "span"
}
t.Run(fmt.Sprintf("%s-%v", testCase.key, testCase.value), func(t *testing.T) {
withTestTracer(func(testTracer *testTracer) {
span := testTracer.tracer.StartSpan("span", ext.SpanKindRPCServer)
span.SetTag(testCase.key, testCase.value)
span.Finish()
u.AssertCounterMetrics(t, testTracer.metrics, testCase.metrics...)
})
})
}
}

View File

@ -0,0 +1,531 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"fmt"
"math"
"net/url"
"sync"
"time"
"github.com/uber/jaeger-client-go/log"
"github.com/uber/jaeger-client-go/thrift-gen/sampling"
"github.com/uber/jaeger-client-go/utils"
)
const (
defaultSamplingServerURL = "http://localhost:5778/sampling"
defaultSamplingRefreshInterval = time.Minute
defaultMaxOperations = 2000
)
// Sampler decides whether a new trace should be sampled or not.
type Sampler interface {
// IsSampled decides whether a trace with given `id` and `operation`
// should be sampled. This function will also return the tags that
// can be used to identify the type of sampling that was applied to
// the root span. Most simple samplers would return two tags,
// sampler.type and sampler.param, similar to those used in the Configuration
IsSampled(id TraceID, operation string) (sampled bool, tags []Tag)
// Close does a clean shutdown of the sampler, stopping any background
// go-routines it may have started.
Close()
// Equal checks if the `other` sampler is functionally equivalent
// to this sampler.
// TODO remove this function. This function is used to determine if 2 samplers are equivalent
// which does not bode well with the adaptive sampler which has to create all the composite samplers
// for the comparison to occur. This is expensive to do if only one sampler has changed.
Equal(other Sampler) bool
}
// -----------------------
// ConstSampler is a sampler that always makes the same decision.
type ConstSampler struct {
Decision bool
tags []Tag
}
// NewConstSampler creates a ConstSampler.
func NewConstSampler(sample bool) Sampler {
tags := []Tag{
{key: SamplerTypeTagKey, value: SamplerTypeConst},
{key: SamplerParamTagKey, value: sample},
}
return &ConstSampler{Decision: sample, tags: tags}
}
// IsSampled implements IsSampled() of Sampler.
func (s *ConstSampler) IsSampled(id TraceID, operation string) (bool, []Tag) {
return s.Decision, s.tags
}
// Close implements Close() of Sampler.
func (s *ConstSampler) Close() {
// nothing to do
}
// Equal implements Equal() of Sampler.
func (s *ConstSampler) Equal(other Sampler) bool {
if o, ok := other.(*ConstSampler); ok {
return s.Decision == o.Decision
}
return false
}
// -----------------------
// ProbabilisticSampler is a sampler that randomly samples a certain percentage
// of traces.
type ProbabilisticSampler struct {
samplingRate float64
samplingBoundary uint64
tags []Tag
}
const maxRandomNumber = ^(uint64(1) << 63) // i.e. 0x7fffffffffffffff
// NewProbabilisticSampler creates a sampler that randomly samples a certain percentage of traces specified by the
// samplingRate, in the range between 0.0 and 1.0.
//
// It relies on the fact that new trace IDs are 63bit random numbers themselves, thus making the sampling decision
// without generating a new random number, but simply calculating if traceID < (samplingRate * 2^63).
// TODO remove the error from this function for next major release
func NewProbabilisticSampler(samplingRate float64) (*ProbabilisticSampler, error) {
if samplingRate < 0.0 || samplingRate > 1.0 {
return nil, fmt.Errorf("Sampling Rate must be between 0.0 and 1.0, received %f", samplingRate)
}
return newProbabilisticSampler(samplingRate), nil
}
func newProbabilisticSampler(samplingRate float64) *ProbabilisticSampler {
samplingRate = math.Max(0.0, math.Min(samplingRate, 1.0))
tags := []Tag{
{key: SamplerTypeTagKey, value: SamplerTypeProbabilistic},
{key: SamplerParamTagKey, value: samplingRate},
}
return &ProbabilisticSampler{
samplingRate: samplingRate,
samplingBoundary: uint64(float64(maxRandomNumber) * samplingRate),
tags: tags,
}
}
// SamplingRate returns the sampling probability this sampled was constructed with.
func (s *ProbabilisticSampler) SamplingRate() float64 {
return s.samplingRate
}
// IsSampled implements IsSampled() of Sampler.
func (s *ProbabilisticSampler) IsSampled(id TraceID, operation string) (bool, []Tag) {
return s.samplingBoundary >= id.Low, s.tags
}
// Close implements Close() of Sampler.
func (s *ProbabilisticSampler) Close() {
// nothing to do
}
// Equal implements Equal() of Sampler.
func (s *ProbabilisticSampler) Equal(other Sampler) bool {
if o, ok := other.(*ProbabilisticSampler); ok {
return s.samplingBoundary == o.samplingBoundary
}
return false
}
// -----------------------
type rateLimitingSampler struct {
maxTracesPerSecond float64
rateLimiter utils.RateLimiter
tags []Tag
}
// NewRateLimitingSampler creates a sampler that samples at most maxTracesPerSecond. The distribution of sampled
// traces follows burstiness of the service, i.e. a service with uniformly distributed requests will have those
// requests sampled uniformly as well, but if requests are bursty, especially sub-second, then a number of
// sequential requests can be sampled each second.
func NewRateLimitingSampler(maxTracesPerSecond float64) Sampler {
tags := []Tag{
{key: SamplerTypeTagKey, value: SamplerTypeRateLimiting},
{key: SamplerParamTagKey, value: maxTracesPerSecond},
}
return &rateLimitingSampler{
maxTracesPerSecond: maxTracesPerSecond,
rateLimiter: utils.NewRateLimiter(maxTracesPerSecond, math.Max(maxTracesPerSecond, 1.0)),
tags: tags,
}
}
// IsSampled implements IsSampled() of Sampler.
func (s *rateLimitingSampler) IsSampled(id TraceID, operation string) (bool, []Tag) {
return s.rateLimiter.CheckCredit(1.0), s.tags
}
func (s *rateLimitingSampler) Close() {
// nothing to do
}
func (s *rateLimitingSampler) Equal(other Sampler) bool {
if o, ok := other.(*rateLimitingSampler); ok {
return s.maxTracesPerSecond == o.maxTracesPerSecond
}
return false
}
// -----------------------
// GuaranteedThroughputProbabilisticSampler is a sampler that leverages both probabilisticSampler and
// rateLimitingSampler. The rateLimitingSampler is used as a guaranteed lower bound sampler such that
// every operation is sampled at least once in a time interval defined by the lowerBound. ie a lowerBound
// of 1.0 / (60 * 10) will sample an operation at least once every 10 minutes.
//
// The probabilisticSampler is given higher priority when tags are emitted, ie. if IsSampled() for both
// samplers return true, the tags for probabilisticSampler will be used.
type GuaranteedThroughputProbabilisticSampler struct {
probabilisticSampler *ProbabilisticSampler
lowerBoundSampler Sampler
tags []Tag
samplingRate float64
lowerBound float64
}
// NewGuaranteedThroughputProbabilisticSampler returns a delegating sampler that applies both
// probabilisticSampler and rateLimitingSampler.
func NewGuaranteedThroughputProbabilisticSampler(
lowerBound, samplingRate float64,
) (*GuaranteedThroughputProbabilisticSampler, error) {
return newGuaranteedThroughputProbabilisticSampler(lowerBound, samplingRate), nil
}
func newGuaranteedThroughputProbabilisticSampler(lowerBound, samplingRate float64) *GuaranteedThroughputProbabilisticSampler {
s := &GuaranteedThroughputProbabilisticSampler{
lowerBoundSampler: NewRateLimitingSampler(lowerBound),
lowerBound: lowerBound,
}
s.setProbabilisticSampler(samplingRate)
return s
}
func (s *GuaranteedThroughputProbabilisticSampler) setProbabilisticSampler(samplingRate float64) {
if s.probabilisticSampler == nil || s.samplingRate != samplingRate {
s.probabilisticSampler = newProbabilisticSampler(samplingRate)
s.samplingRate = s.probabilisticSampler.SamplingRate()
s.tags = []Tag{
{key: SamplerTypeTagKey, value: SamplerTypeLowerBound},
{key: SamplerParamTagKey, value: s.samplingRate},
}
}
}
// IsSampled implements IsSampled() of Sampler.
func (s *GuaranteedThroughputProbabilisticSampler) IsSampled(id TraceID, operation string) (bool, []Tag) {
if sampled, tags := s.probabilisticSampler.IsSampled(id, operation); sampled {
s.lowerBoundSampler.IsSampled(id, operation)
return true, tags
}
sampled, _ := s.lowerBoundSampler.IsSampled(id, operation)
return sampled, s.tags
}
// Close implements Close() of Sampler.
func (s *GuaranteedThroughputProbabilisticSampler) Close() {
s.probabilisticSampler.Close()
s.lowerBoundSampler.Close()
}
// Equal implements Equal() of Sampler.
func (s *GuaranteedThroughputProbabilisticSampler) Equal(other Sampler) bool {
// NB The Equal() function is expensive and will be removed. See adaptiveSampler.Equal() for
// more information.
return false
}
// this function should only be called while holding a Write lock
func (s *GuaranteedThroughputProbabilisticSampler) update(lowerBound, samplingRate float64) {
s.setProbabilisticSampler(samplingRate)
if s.lowerBound != lowerBound {
s.lowerBoundSampler = NewRateLimitingSampler(lowerBound)
s.lowerBound = lowerBound
}
}
// -----------------------
type adaptiveSampler struct {
sync.RWMutex
samplers map[string]*GuaranteedThroughputProbabilisticSampler
defaultSampler *ProbabilisticSampler
lowerBound float64
maxOperations int
}
// NewAdaptiveSampler returns a delegating sampler that applies both probabilisticSampler and
// rateLimitingSampler via the guaranteedThroughputProbabilisticSampler. This sampler keeps track of all
// operations and delegates calls to the respective guaranteedThroughputProbabilisticSampler.
func NewAdaptiveSampler(strategies *sampling.PerOperationSamplingStrategies, maxOperations int) (Sampler, error) {
return newAdaptiveSampler(strategies, maxOperations), nil
}
func newAdaptiveSampler(strategies *sampling.PerOperationSamplingStrategies, maxOperations int) Sampler {
samplers := make(map[string]*GuaranteedThroughputProbabilisticSampler)
for _, strategy := range strategies.PerOperationStrategies {
sampler := newGuaranteedThroughputProbabilisticSampler(
strategies.DefaultLowerBoundTracesPerSecond,
strategy.ProbabilisticSampling.SamplingRate,
)
samplers[strategy.Operation] = sampler
}
return &adaptiveSampler{
samplers: samplers,
defaultSampler: newProbabilisticSampler(strategies.DefaultSamplingProbability),
lowerBound: strategies.DefaultLowerBoundTracesPerSecond,
maxOperations: maxOperations,
}
}
func (s *adaptiveSampler) IsSampled(id TraceID, operation string) (bool, []Tag) {
s.RLock()
sampler, ok := s.samplers[operation]
if ok {
defer s.RUnlock()
return sampler.IsSampled(id, operation)
}
s.RUnlock()
s.Lock()
defer s.Unlock()
// Check if sampler has already been created
sampler, ok = s.samplers[operation]
if ok {
return sampler.IsSampled(id, operation)
}
// Store only up to maxOperations of unique ops.
if len(s.samplers) >= s.maxOperations {
return s.defaultSampler.IsSampled(id, operation)
}
newSampler := newGuaranteedThroughputProbabilisticSampler(s.lowerBound, s.defaultSampler.SamplingRate())
s.samplers[operation] = newSampler
return newSampler.IsSampled(id, operation)
}
func (s *adaptiveSampler) Close() {
s.Lock()
defer s.Unlock()
for _, sampler := range s.samplers {
sampler.Close()
}
s.defaultSampler.Close()
}
func (s *adaptiveSampler) Equal(other Sampler) bool {
// NB The Equal() function is overly expensive for adaptiveSampler since it's composed of multiple
// samplers which all need to be initialized before this function can be called for a comparison.
// Therefore, adaptiveSampler uses the update() function to only alter the samplers that need
// changing. Hence this function always returns false so that the update function can be called.
// Once the Equal() function is removed from the Sampler API, this will no longer be needed.
return false
}
func (s *adaptiveSampler) update(strategies *sampling.PerOperationSamplingStrategies) {
s.Lock()
defer s.Unlock()
for _, strategy := range strategies.PerOperationStrategies {
operation := strategy.Operation
samplingRate := strategy.ProbabilisticSampling.SamplingRate
lowerBound := strategies.DefaultLowerBoundTracesPerSecond
if sampler, ok := s.samplers[operation]; ok {
sampler.update(lowerBound, samplingRate)
} else {
sampler := newGuaranteedThroughputProbabilisticSampler(
lowerBound,
samplingRate,
)
s.samplers[operation] = sampler
}
}
s.lowerBound = strategies.DefaultLowerBoundTracesPerSecond
if s.defaultSampler.SamplingRate() != strategies.DefaultSamplingProbability {
s.defaultSampler = newProbabilisticSampler(strategies.DefaultSamplingProbability)
}
}
// -----------------------
// RemotelyControlledSampler is a delegating sampler that polls a remote server
// for the appropriate sampling strategy, constructs a corresponding sampler and
// delegates to it for sampling decisions.
type RemotelyControlledSampler struct {
sync.RWMutex
samplerOptions
serviceName string
timer *time.Ticker
manager sampling.SamplingManager
pollStopped sync.WaitGroup
}
type httpSamplingManager struct {
serverURL string
}
func (s *httpSamplingManager) GetSamplingStrategy(serviceName string) (*sampling.SamplingStrategyResponse, error) {
var out sampling.SamplingStrategyResponse
v := url.Values{}
v.Set("service", serviceName)
if err := utils.GetJSON(s.serverURL+"?"+v.Encode(), &out); err != nil {
return nil, err
}
return &out, nil
}
// NewRemotelyControlledSampler creates a sampler that periodically pulls
// the sampling strategy from an HTTP sampling server (e.g. jaeger-agent).
func NewRemotelyControlledSampler(
serviceName string,
opts ...SamplerOption,
) *RemotelyControlledSampler {
options := applySamplerOptions(opts...)
sampler := &RemotelyControlledSampler{
samplerOptions: options,
serviceName: serviceName,
timer: time.NewTicker(options.samplingRefreshInterval),
manager: &httpSamplingManager{serverURL: options.samplingServerURL},
}
go sampler.pollController()
return sampler
}
func applySamplerOptions(opts ...SamplerOption) samplerOptions {
options := samplerOptions{}
for _, option := range opts {
option(&options)
}
if options.sampler == nil {
options.sampler = newProbabilisticSampler(0.001)
}
if options.logger == nil {
options.logger = log.NullLogger
}
if options.maxOperations <= 0 {
options.maxOperations = defaultMaxOperations
}
if options.samplingServerURL == "" {
options.samplingServerURL = defaultSamplingServerURL
}
if options.metrics == nil {
options.metrics = NewNullMetrics()
}
if options.samplingRefreshInterval <= 0 {
options.samplingRefreshInterval = defaultSamplingRefreshInterval
}
return options
}
// IsSampled implements IsSampled() of Sampler.
func (s *RemotelyControlledSampler) IsSampled(id TraceID, operation string) (bool, []Tag) {
s.RLock()
defer s.RUnlock()
return s.sampler.IsSampled(id, operation)
}
// Close implements Close() of Sampler.
func (s *RemotelyControlledSampler) Close() {
s.RLock()
s.timer.Stop()
s.RUnlock()
s.pollStopped.Wait()
}
// Equal implements Equal() of Sampler.
func (s *RemotelyControlledSampler) Equal(other Sampler) bool {
// NB The Equal() function is expensive and will be removed. See adaptiveSampler.Equal() for
// more information.
if o, ok := other.(*RemotelyControlledSampler); ok {
s.RLock()
o.RLock()
defer s.RUnlock()
defer o.RUnlock()
return s.sampler.Equal(o.sampler)
}
return false
}
func (s *RemotelyControlledSampler) pollController() {
// in unit tests we re-assign the timer Ticker, so need to lock to avoid data races
s.Lock()
timer := s.timer
s.Unlock()
for range timer.C {
s.updateSampler()
}
s.pollStopped.Add(1)
}
func (s *RemotelyControlledSampler) updateSampler() {
res, err := s.manager.GetSamplingStrategy(s.serviceName)
if err != nil {
s.metrics.SamplerQueryFailure.Inc(1)
return
}
s.Lock()
defer s.Unlock()
s.metrics.SamplerRetrieved.Inc(1)
if strategies := res.GetOperationSampling(); strategies != nil {
s.updateAdaptiveSampler(strategies)
} else {
err = s.updateRateLimitingOrProbabilisticSampler(res)
}
if err != nil {
s.metrics.SamplerUpdateFailure.Inc(1)
s.logger.Infof("Unable to handle sampling strategy response %+v. Got error: %v", res, err)
return
}
s.metrics.SamplerUpdated.Inc(1)
}
// NB: this function should only be called while holding a Write lock
func (s *RemotelyControlledSampler) updateAdaptiveSampler(strategies *sampling.PerOperationSamplingStrategies) {
if adaptiveSampler, ok := s.sampler.(*adaptiveSampler); ok {
adaptiveSampler.update(strategies)
} else {
s.sampler = newAdaptiveSampler(strategies, s.maxOperations)
}
}
// NB: this function should only be called while holding a Write lock
func (s *RemotelyControlledSampler) updateRateLimitingOrProbabilisticSampler(res *sampling.SamplingStrategyResponse) error {
var newSampler Sampler
if probabilistic := res.GetProbabilisticSampling(); probabilistic != nil {
newSampler = newProbabilisticSampler(probabilistic.SamplingRate)
} else if rateLimiting := res.GetRateLimitingSampling(); rateLimiting != nil {
newSampler = NewRateLimitingSampler(float64(rateLimiting.MaxTracesPerSecond))
} else {
return fmt.Errorf("Unsupported sampling strategy type %v", res.GetStrategyType())
}
if !s.sampler.Equal(newSampler) {
s.sampler = newSampler
}
return nil
}

View File

@ -0,0 +1,81 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"time"
)
// SamplerOption is a function that sets some option on the sampler
type SamplerOption func(options *samplerOptions)
// SamplerOptions is a factory for all available SamplerOption's
var SamplerOptions samplerOptions
type samplerOptions struct {
metrics *Metrics
maxOperations int
sampler Sampler
logger Logger
samplingServerURL string
samplingRefreshInterval time.Duration
}
// Metrics creates a SamplerOption that initializes Metrics on the sampler,
// which is used to emit statistics.
func (samplerOptions) Metrics(m *Metrics) SamplerOption {
return func(o *samplerOptions) {
o.metrics = m
}
}
// MaxOperations creates a SamplerOption that sets the maximum number of
// operations the sampler will keep track of.
func (samplerOptions) MaxOperations(maxOperations int) SamplerOption {
return func(o *samplerOptions) {
o.maxOperations = maxOperations
}
}
// InitialSampler creates a SamplerOption that sets the initial sampler
// to use before a remote sampler is created and used.
func (samplerOptions) InitialSampler(sampler Sampler) SamplerOption {
return func(o *samplerOptions) {
o.sampler = sampler
}
}
// Logger creates a SamplerOption that sets the logger used by the sampler.
func (samplerOptions) Logger(logger Logger) SamplerOption {
return func(o *samplerOptions) {
o.logger = logger
}
}
// SamplingServerURL creates a SamplerOption that sets the sampling server url
// of the local agent that contains the sampling strategies.
func (samplerOptions) SamplingServerURL(samplingServerURL string) SamplerOption {
return func(o *samplerOptions) {
o.samplingServerURL = samplingServerURL
}
}
// SamplingRefreshInterval creates a SamplerOption that sets how often the
// sampler will poll local agent for the appropriate sampling strategy.
func (samplerOptions) SamplingRefreshInterval(samplingRefreshInterval time.Duration) SamplerOption {
return func(o *samplerOptions) {
o.samplingRefreshInterval = samplingRefreshInterval
}
}

View File

@ -0,0 +1,703 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"errors"
"fmt"
"runtime"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-lib/metrics"
mTestutils "github.com/uber/jaeger-lib/metrics/testutils"
"github.com/uber/jaeger-client-go/log"
"github.com/uber/jaeger-client-go/testutils"
"github.com/uber/jaeger-client-go/thrift-gen/sampling"
"github.com/uber/jaeger-client-go/utils"
)
const (
testOperationName = "op"
testFirstTimeOperationName = "firstTimeOp"
testDefaultSamplingProbability = 0.5
testMaxID = uint64(1) << 62
testDefaultMaxOperations = 10
)
var (
testProbabilisticExpectedTags = []Tag{
{"sampler.type", "probabilistic"},
{"sampler.param", 0.5},
}
testLowerBoundExpectedTags = []Tag{
{"sampler.type", "lowerbound"},
{"sampler.param", 0.5},
}
)
func TestSamplerTags(t *testing.T) {
prob, err := NewProbabilisticSampler(0.1)
require.NoError(t, err)
rate := NewRateLimitingSampler(0.1)
remote := &RemotelyControlledSampler{}
remote.sampler = NewConstSampler(true)
tests := []struct {
sampler Sampler
typeTag string
paramTag interface{}
}{
{NewConstSampler(true), "const", true},
{NewConstSampler(false), "const", false},
{prob, "probabilistic", 0.1},
{rate, "ratelimiting", 0.1},
{remote, "const", true},
}
for _, test := range tests {
_, tags := test.sampler.IsSampled(TraceID{}, testOperationName)
count := 0
for _, tag := range tags {
if tag.key == SamplerTypeTagKey {
assert.Equal(t, test.typeTag, tag.value)
count++
}
if tag.key == SamplerParamTagKey {
assert.Equal(t, test.paramTag, tag.value)
count++
}
}
assert.Equal(t, 2, count)
}
}
func TestApplySamplerOptions(t *testing.T) {
options := applySamplerOptions()
sampler, ok := options.sampler.(*ProbabilisticSampler)
assert.True(t, ok)
assert.Equal(t, 0.001, sampler.samplingRate)
assert.NotNil(t, options.logger)
assert.NotZero(t, options.maxOperations)
assert.NotEmpty(t, options.samplingServerURL)
assert.NotNil(t, options.metrics)
assert.NotZero(t, options.samplingRefreshInterval)
}
func TestProbabilisticSamplerErrors(t *testing.T) {
_, err := NewProbabilisticSampler(-0.1)
assert.Error(t, err)
_, err = NewProbabilisticSampler(1.1)
assert.Error(t, err)
}
func TestProbabilisticSampler(t *testing.T) {
sampler, _ := NewProbabilisticSampler(0.5)
sampled, tags := sampler.IsSampled(TraceID{Low: testMaxID + 10}, testOperationName)
assert.False(t, sampled)
assert.Equal(t, testProbabilisticExpectedTags, tags)
sampled, tags = sampler.IsSampled(TraceID{Low: testMaxID - 20}, testOperationName)
assert.True(t, sampled)
assert.Equal(t, testProbabilisticExpectedTags, tags)
sampler2, _ := NewProbabilisticSampler(0.5)
assert.True(t, sampler.Equal(sampler2))
assert.False(t, sampler.Equal(NewConstSampler(true)))
}
func TestProbabilisticSamplerPerformance(t *testing.T) {
t.Skip("Skipped performance test")
sampler, _ := NewProbabilisticSampler(0.01)
rand := utils.NewRand(8736823764)
var count uint64
for i := 0; i < 100000000; i++ {
id := TraceID{Low: uint64(rand.Int63())}
if sampled, _ := sampler.IsSampled(id, testOperationName); sampled {
count++
}
}
println("Sampled:", count, "rate=", float64(count)/float64(100000000))
// Sampled: 999829 rate= 0.009998290
}
func TestRateLimitingSampler(t *testing.T) {
sampler := NewRateLimitingSampler(2)
sampler2 := NewRateLimitingSampler(2)
sampler3 := NewRateLimitingSampler(3)
assert.True(t, sampler.Equal(sampler2))
assert.False(t, sampler.Equal(sampler3))
assert.False(t, sampler.Equal(NewConstSampler(false)))
sampler = NewRateLimitingSampler(2)
sampled, _ := sampler.IsSampled(TraceID{}, testOperationName)
assert.True(t, sampled)
sampled, _ = sampler.IsSampled(TraceID{}, testOperationName)
assert.True(t, sampled)
sampled, _ = sampler.IsSampled(TraceID{}, testOperationName)
assert.False(t, sampled)
sampler = NewRateLimitingSampler(0.1)
sampled, _ = sampler.IsSampled(TraceID{}, testOperationName)
assert.True(t, sampled)
sampled, _ = sampler.IsSampled(TraceID{}, testOperationName)
assert.False(t, sampled)
}
func TestGuaranteedThroughputProbabilisticSamplerUpdate(t *testing.T) {
samplingRate := 0.5
lowerBound := 2.0
sampler, err := NewGuaranteedThroughputProbabilisticSampler(lowerBound, samplingRate)
assert.NoError(t, err)
assert.Equal(t, lowerBound, sampler.lowerBound)
assert.Equal(t, samplingRate, sampler.samplingRate)
newSamplingRate := 0.6
newLowerBound := 1.0
sampler.update(newLowerBound, newSamplingRate)
assert.Equal(t, newLowerBound, sampler.lowerBound)
assert.Equal(t, newSamplingRate, sampler.samplingRate)
newSamplingRate = 1.1
sampler.update(newLowerBound, newSamplingRate)
assert.Equal(t, 1.0, sampler.samplingRate)
}
func TestAdaptiveSampler(t *testing.T) {
samplingRates := []*sampling.OperationSamplingStrategy{
{
Operation: testOperationName,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: testDefaultSamplingProbability},
},
}
strategies := &sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: testDefaultSamplingProbability,
DefaultLowerBoundTracesPerSecond: 1.0,
PerOperationStrategies: samplingRates,
}
sampler, err := NewAdaptiveSampler(strategies, testDefaultMaxOperations)
require.NoError(t, err)
defer sampler.Close()
sampled, tags := sampler.IsSampled(TraceID{Low: testMaxID + 10}, testOperationName)
assert.True(t, sampled)
assert.Equal(t, testLowerBoundExpectedTags, tags)
sampled, tags = sampler.IsSampled(TraceID{Low: testMaxID - 20}, testOperationName)
assert.True(t, sampled)
assert.Equal(t, testProbabilisticExpectedTags, tags)
sampled, tags = sampler.IsSampled(TraceID{Low: testMaxID + 10}, testOperationName)
assert.False(t, sampled)
// This operation is seen for the first time by the sampler
sampled, tags = sampler.IsSampled(TraceID{Low: testMaxID}, testFirstTimeOperationName)
assert.True(t, sampled)
assert.Equal(t, testProbabilisticExpectedTags, tags)
}
func TestAdaptiveSamplerErrors(t *testing.T) {
strategies := &sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: testDefaultSamplingProbability,
DefaultLowerBoundTracesPerSecond: 2.0,
PerOperationStrategies: []*sampling.OperationSamplingStrategy{
{
Operation: testOperationName,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: -0.1},
},
},
}
sampler, err := NewAdaptiveSampler(strategies, testDefaultMaxOperations)
assert.NoError(t, err)
assert.Equal(t, 0.0, sampler.(*adaptiveSampler).samplers[testOperationName].samplingRate)
strategies.PerOperationStrategies[0].ProbabilisticSampling.SamplingRate = 1.1
sampler, err = NewAdaptiveSampler(strategies, testDefaultMaxOperations)
assert.NoError(t, err)
assert.Equal(t, 1.0, sampler.(*adaptiveSampler).samplers[testOperationName].samplingRate)
}
func TestAdaptiveSamplerUpdate(t *testing.T) {
samplingRate := 0.1
lowerBound := 2.0
samplingRates := []*sampling.OperationSamplingStrategy{
{
Operation: testOperationName,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: samplingRate},
},
}
strategies := &sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: testDefaultSamplingProbability,
DefaultLowerBoundTracesPerSecond: lowerBound,
PerOperationStrategies: samplingRates,
}
s, err := NewAdaptiveSampler(strategies, testDefaultMaxOperations)
assert.NoError(t, err)
sampler, ok := s.(*adaptiveSampler)
assert.True(t, ok)
assert.Equal(t, lowerBound, sampler.lowerBound)
assert.Equal(t, testDefaultSamplingProbability, sampler.defaultSampler.SamplingRate())
assert.Len(t, sampler.samplers, 1)
// Update the sampler with new sampling rates
newSamplingRate := 0.2
newLowerBound := 3.0
newDefaultSamplingProbability := 0.1
newSamplingRates := []*sampling.OperationSamplingStrategy{
{
Operation: testOperationName,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: newSamplingRate},
},
{
Operation: testFirstTimeOperationName,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: newSamplingRate},
},
}
strategies = &sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: newDefaultSamplingProbability,
DefaultLowerBoundTracesPerSecond: newLowerBound,
PerOperationStrategies: newSamplingRates,
}
sampler.update(strategies)
assert.Equal(t, newLowerBound, sampler.lowerBound)
assert.Equal(t, newDefaultSamplingProbability, sampler.defaultSampler.SamplingRate())
assert.Len(t, sampler.samplers, 2)
}
func initAgent(t *testing.T) (*testutils.MockAgent, *RemotelyControlledSampler, *metrics.LocalFactory) {
agent, err := testutils.StartMockAgent()
require.NoError(t, err)
metricsFactory := metrics.NewLocalFactory(0)
metrics := NewMetrics(metricsFactory, nil)
initialSampler, _ := NewProbabilisticSampler(0.001)
sampler := NewRemotelyControlledSampler(
"client app",
SamplerOptions.Metrics(metrics),
SamplerOptions.SamplingServerURL("http://"+agent.SamplingServerAddr()),
SamplerOptions.MaxOperations(testDefaultMaxOperations),
SamplerOptions.InitialSampler(initialSampler),
SamplerOptions.Logger(log.NullLogger),
SamplerOptions.SamplingRefreshInterval(time.Minute),
)
sampler.Close() // stop timer-based updates, we want to call them manually
return agent, sampler, metricsFactory
}
func TestRemotelyControlledSampler(t *testing.T) {
agent, remoteSampler, metricsFactory := initAgent(t)
defer agent.Close()
initSampler, ok := remoteSampler.sampler.(*ProbabilisticSampler)
assert.True(t, ok)
agent.AddSamplingStrategy("client app",
getSamplingStrategyResponse(sampling.SamplingStrategyType_PROBABILISTIC, testDefaultSamplingProbability))
remoteSampler.updateSampler()
mTestutils.AssertCounterMetrics(t, metricsFactory, []mTestutils.ExpectedMetric{
{Name: "jaeger.sampler", Tags: map[string]string{"state": "retrieved"}, Value: 1},
{Name: "jaeger.sampler", Tags: map[string]string{"state": "updated"}, Value: 1},
}...)
_, ok = remoteSampler.sampler.(*ProbabilisticSampler)
assert.True(t, ok)
assert.NotEqual(t, initSampler, remoteSampler.sampler, "Sampler should have been updated")
sampled, tags := remoteSampler.IsSampled(TraceID{Low: testMaxID + 10}, testOperationName)
assert.False(t, sampled)
assert.Equal(t, testProbabilisticExpectedTags, tags)
sampled, tags = remoteSampler.IsSampled(TraceID{Low: testMaxID - 10}, testOperationName)
assert.True(t, sampled)
assert.Equal(t, testProbabilisticExpectedTags, tags)
remoteSampler.sampler = initSampler
c := make(chan time.Time)
remoteSampler.Lock()
remoteSampler.timer = &time.Ticker{C: c}
remoteSampler.Unlock()
go remoteSampler.pollController()
c <- time.Now() // force update based on timer
time.Sleep(10 * time.Millisecond)
remoteSampler.Close()
_, ok = remoteSampler.sampler.(*ProbabilisticSampler)
assert.True(t, ok)
assert.NotEqual(t, initSampler, remoteSampler.sampler, "Sampler should have been updated from timer")
assert.True(t, remoteSampler.Equal(remoteSampler))
}
func generateTags(key string, value float64) []Tag {
return []Tag{
{"sampler.type", key},
{"sampler.param", value},
}
}
func TestRemotelyControlledSampler_updateSampler(t *testing.T) {
tests := []struct {
probabilities map[string]float64
defaultProbability float64
expectedDefaultProbability float64
expectedTags []Tag
}{
{
probabilities: map[string]float64{testOperationName: 1.1},
defaultProbability: testDefaultSamplingProbability,
expectedDefaultProbability: testDefaultSamplingProbability,
expectedTags: generateTags("probabilistic", 1.0),
},
{
probabilities: map[string]float64{testOperationName: testDefaultSamplingProbability},
defaultProbability: testDefaultSamplingProbability,
expectedDefaultProbability: testDefaultSamplingProbability,
expectedTags: testProbabilisticExpectedTags,
},
{
probabilities: map[string]float64{
testOperationName: testDefaultSamplingProbability,
testFirstTimeOperationName: testDefaultSamplingProbability,
},
defaultProbability: testDefaultSamplingProbability,
expectedDefaultProbability: testDefaultSamplingProbability,
expectedTags: testProbabilisticExpectedTags,
},
{
probabilities: map[string]float64{"new op": 1.1},
defaultProbability: testDefaultSamplingProbability,
expectedDefaultProbability: testDefaultSamplingProbability,
expectedTags: testProbabilisticExpectedTags,
},
{
probabilities: map[string]float64{"new op": 1.1},
defaultProbability: 1.1,
expectedDefaultProbability: 1.0,
expectedTags: generateTags("probabilistic", 1.0),
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
agent, sampler, metricsFactory := initAgent(t)
defer agent.Close()
initSampler, ok := sampler.sampler.(*ProbabilisticSampler)
assert.True(t, ok)
res := &sampling.SamplingStrategyResponse{
StrategyType: sampling.SamplingStrategyType_PROBABILISTIC,
OperationSampling: &sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: test.defaultProbability,
DefaultLowerBoundTracesPerSecond: 0.001,
},
}
for opName, prob := range test.probabilities {
res.OperationSampling.PerOperationStrategies = append(res.OperationSampling.PerOperationStrategies,
&sampling.OperationSamplingStrategy{
Operation: opName,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{
SamplingRate: prob,
},
},
)
}
agent.AddSamplingStrategy("client app", res)
sampler.updateSampler()
mTestutils.AssertCounterMetrics(t, metricsFactory,
mTestutils.ExpectedMetric{
Name: "jaeger.sampler" + "|state=updated", Value: 1,
},
)
s, ok := sampler.sampler.(*adaptiveSampler)
assert.True(t, ok)
assert.NotEqual(t, initSampler, sampler.sampler, "Sampler should have been updated")
assert.Equal(t, test.expectedDefaultProbability, s.defaultSampler.SamplingRate())
// First call is always sampled
sampled, tags := sampler.IsSampled(TraceID{Low: testMaxID + 10}, testOperationName)
assert.True(t, sampled)
sampled, tags = sampler.IsSampled(TraceID{Low: testMaxID - 10}, testOperationName)
assert.True(t, sampled)
assert.Equal(t, test.expectedTags, tags)
})
}
}
func TestMaxOperations(t *testing.T) {
samplingRates := []*sampling.OperationSamplingStrategy{
{
Operation: testOperationName,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{SamplingRate: 0.1},
},
}
strategies := &sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: testDefaultSamplingProbability,
DefaultLowerBoundTracesPerSecond: 2.0,
PerOperationStrategies: samplingRates,
}
sampler, err := NewAdaptiveSampler(strategies, 1)
assert.NoError(t, err)
sampled, tags := sampler.IsSampled(TraceID{Low: testMaxID - 10}, testFirstTimeOperationName)
assert.True(t, sampled)
assert.Equal(t, testProbabilisticExpectedTags, tags)
}
func TestSamplerQueryError(t *testing.T) {
agent, sampler, metricsFactory := initAgent(t)
defer agent.Close()
// override the actual handler
sampler.manager = &fakeSamplingManager{}
initSampler, ok := sampler.sampler.(*ProbabilisticSampler)
assert.True(t, ok)
sampler.Close() // stop timer-based updates, we want to call them manually
sampler.updateSampler()
assert.Equal(t, initSampler, sampler.sampler, "Sampler should not have been updated due to query error")
mTestutils.AssertCounterMetrics(t, metricsFactory,
mTestutils.ExpectedMetric{
Name: "jaeger.sampler",
Tags: map[string]string{"phase": "query", "state": "failure"},
Value: 1,
},
)
}
type fakeSamplingManager struct{}
func (c *fakeSamplingManager) GetSamplingStrategy(serviceName string) (*sampling.SamplingStrategyResponse, error) {
return nil, errors.New("query error")
}
func TestRemotelyControlledSampler_updateSamplerFromAdaptiveSampler(t *testing.T) {
agent, remoteSampler, metricsFactory := initAgent(t)
defer agent.Close()
remoteSampler.Close() // stop timer-based updates, we want to call them manually
strategies := &sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: testDefaultSamplingProbability,
DefaultLowerBoundTracesPerSecond: 1.0,
}
adaptiveSampler, err := NewAdaptiveSampler(strategies, testDefaultMaxOperations)
require.NoError(t, err)
// Overwrite the sampler with an adaptive sampler
remoteSampler.sampler = adaptiveSampler
agent.AddSamplingStrategy("client app",
getSamplingStrategyResponse(sampling.SamplingStrategyType_PROBABILISTIC, 0.5))
remoteSampler.updateSampler()
// Sampler should have been updated to probabilistic
_, ok := remoteSampler.sampler.(*ProbabilisticSampler)
require.True(t, ok)
// Overwrite the sampler with an adaptive sampler
remoteSampler.sampler = adaptiveSampler
agent.AddSamplingStrategy("client app",
getSamplingStrategyResponse(sampling.SamplingStrategyType_RATE_LIMITING, 1))
remoteSampler.updateSampler()
// Sampler should have been updated to ratelimiting
_, ok = remoteSampler.sampler.(*rateLimitingSampler)
require.True(t, ok)
// Overwrite the sampler with an adaptive sampler
remoteSampler.sampler = adaptiveSampler
// Update existing adaptive sampler
agent.AddSamplingStrategy("client app", &sampling.SamplingStrategyResponse{OperationSampling: strategies})
remoteSampler.updateSampler()
mTestutils.AssertCounterMetrics(t, metricsFactory,
mTestutils.ExpectedMetric{
Name: "jaeger.sampler",
Tags: map[string]string{"state": "retrieved"},
Value: 3,
},
mTestutils.ExpectedMetric{
Name: "jaeger.sampler",
Tags: map[string]string{"state": "updated"},
Value: 3,
},
)
}
func TestRemotelyControlledSampler_updateRateLimitingOrProbabilisticSampler(t *testing.T) {
probabilisticSampler, err := NewProbabilisticSampler(0.002)
require.NoError(t, err)
otherProbabilisticSampler, err := NewProbabilisticSampler(0.003)
require.NoError(t, err)
maxProbabilisticSampler, err := NewProbabilisticSampler(1.0)
require.NoError(t, err)
rateLimitingSampler := NewRateLimitingSampler(2)
otherRateLimitingSampler := NewRateLimitingSampler(3)
testCases := []struct {
res *sampling.SamplingStrategyResponse
initSampler Sampler
expectedSampler Sampler
shouldErr bool
referenceEquivalence bool
caption string
}{
{
res: getSamplingStrategyResponse(sampling.SamplingStrategyType_PROBABILISTIC, 1.5),
initSampler: probabilisticSampler,
expectedSampler: maxProbabilisticSampler,
shouldErr: false,
referenceEquivalence: false,
caption: "invalid probabilistic strategy",
},
{
res: getSamplingStrategyResponse(sampling.SamplingStrategyType_PROBABILISTIC, 0.002),
initSampler: probabilisticSampler,
expectedSampler: probabilisticSampler,
shouldErr: false,
referenceEquivalence: true,
caption: "unchanged probabilistic strategy",
},
{
res: getSamplingStrategyResponse(sampling.SamplingStrategyType_PROBABILISTIC, 0.003),
initSampler: probabilisticSampler,
expectedSampler: otherProbabilisticSampler,
shouldErr: false,
referenceEquivalence: false,
caption: "valid probabilistic strategy",
},
{
res: getSamplingStrategyResponse(sampling.SamplingStrategyType_RATE_LIMITING, 2),
initSampler: rateLimitingSampler,
expectedSampler: rateLimitingSampler,
shouldErr: false,
referenceEquivalence: true,
caption: "unchanged rate limiting strategy",
},
{
res: getSamplingStrategyResponse(sampling.SamplingStrategyType_RATE_LIMITING, 3),
initSampler: rateLimitingSampler,
expectedSampler: otherRateLimitingSampler,
shouldErr: false,
referenceEquivalence: false,
caption: "valid rate limiting strategy",
},
{
res: &sampling.SamplingStrategyResponse{},
initSampler: rateLimitingSampler,
expectedSampler: rateLimitingSampler,
shouldErr: true,
referenceEquivalence: true,
caption: "invalid strategy",
},
}
for _, tc := range testCases {
testCase := tc // capture loop var
t.Run(testCase.caption, func(t *testing.T) {
remoteSampler := &RemotelyControlledSampler{samplerOptions: samplerOptions{sampler: testCase.initSampler}}
err := remoteSampler.updateRateLimitingOrProbabilisticSampler(testCase.res)
if testCase.shouldErr {
require.Error(t, err)
}
if testCase.referenceEquivalence {
assert.Equal(t, testCase.expectedSampler, remoteSampler.sampler)
} else {
assert.True(t, testCase.expectedSampler.Equal(remoteSampler.sampler))
}
})
}
}
func getSamplingStrategyResponse(strategyType sampling.SamplingStrategyType, value float64) *sampling.SamplingStrategyResponse {
if strategyType == sampling.SamplingStrategyType_PROBABILISTIC {
return &sampling.SamplingStrategyResponse{
StrategyType: sampling.SamplingStrategyType_PROBABILISTIC,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{
SamplingRate: value,
},
}
}
if strategyType == sampling.SamplingStrategyType_RATE_LIMITING {
return &sampling.SamplingStrategyResponse{
StrategyType: sampling.SamplingStrategyType_RATE_LIMITING,
RateLimitingSampling: &sampling.RateLimitingSamplingStrategy{
MaxTracesPerSecond: int16(value),
},
}
}
return nil
}
func TestAdaptiveSampler_lockRaceCondition(t *testing.T) {
agent, remoteSampler, _ := initAgent(t)
defer agent.Close()
remoteSampler.Close() // stop timer-based updates, we want to call them manually
numOperations := 1000
adaptiveSampler, err := NewAdaptiveSampler(
&sampling.PerOperationSamplingStrategies{
DefaultSamplingProbability: 1,
},
2000,
)
require.NoError(t, err)
// Overwrite the sampler with an adaptive sampler
remoteSampler.sampler = adaptiveSampler
var wg sync.WaitGroup
defer wg.Wait()
wg.Add(2)
// Start 2 go routines that will simulate simultaneous calls to IsSampled
go func() {
defer wg.Done()
isSampled(t, remoteSampler, numOperations, "a")
}()
go func() {
defer wg.Done()
isSampled(t, remoteSampler, numOperations, "b")
}()
}
func isSampled(t *testing.T, remoteSampler *RemotelyControlledSampler, numOperations int, operationNamePrefix string) {
for i := 0; i < numOperations; i++ {
runtime.Gosched()
sampled, _ := remoteSampler.IsSampled(TraceID{}, fmt.Sprintf("%s%d", operationNamePrefix, i))
assert.True(t, sampled)
}
}

View File

@ -0,0 +1,60 @@
#!/bin/bash
set -e
COVER=.cover
ROOT_PKG=github.com/uber/jaeger-client-go/
if [[ -d "$COVER" ]]; then
rm -rf "$COVER"
fi
mkdir -p "$COVER"
# If a package directory has a .nocover file, don't count it when calculating
# coverage.
filter=""
for pkg in "$@"; do
if [[ -f "$GOPATH/src/$pkg/.nocover" ]]; then
if [[ -n "$filter" ]]; then
filter="$filter, "
fi
filter="\"$pkg\": true"
fi
done
i=0
for pkg in "$@"; do
i=$((i + 1))
extracoverpkg=""
if [[ -f "$GOPATH/src/$pkg/.extra-coverpkg" ]]; then
extracoverpkg=$( \
sed -e "s|^|$pkg/|g" < "$GOPATH/src/$pkg/.extra-coverpkg" \
| tr '\n' ',')
fi
coverpkg=$(go list -json "$pkg" | jq -r '
.Deps
| . + ["'"$pkg"'"]
| map
( select(startswith("'"$ROOT_PKG"'"))
| select(contains("/vendor/") | not)
| select(in({'"$filter"'}) | not)
)
| join(",")
')
if [[ -n "$extracoverpkg" ]]; then
coverpkg="$extracoverpkg$coverpkg"
fi
args=""
if [[ -n "$coverpkg" ]]; then
args="-coverprofile $COVER/cover.${i}.out" # -coverpkg $coverpkg"
fi
echo go test -v -race "$pkg"
go test $args -v -race "$pkg"
done
gocovmerge "$COVER"/*.out > cover.out

View File

@ -0,0 +1,120 @@
from __future__ import (
absolute_import, print_function, division, unicode_literals
)
import logging
import re
import sys
from datetime import datetime
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
CURRENT_YEAR = datetime.today().year
MIT_LICENSE_BLOB = """Copyright (c) %d Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.""" % CURRENT_YEAR
LICENSE_BLOB = """Copyright (c) %d Uber Technologies, Inc.
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.""" % CURRENT_YEAR
MIT_LICENSE_BLOB_LINES_GO = [
('// ' + l).strip() + '\n' for l in MIT_LICENSE_BLOB.split('\n')
]
LICENSE_BLOB_LINES_GO = [
('// ' + l).strip() + '\n' for l in LICENSE_BLOB.split('\n')
]
COPYRIGHT_RE = re.compile(r'Copyright \(c\) (\d+)', re.I)
def update_go_license(name, force=False):
with open(name) as f:
orig_lines = list(f)
lines = list(orig_lines)
current_header = ''.join(lines[0:len(MIT_LICENSE_BLOB_LINES_GO)])
mit_header = ''.join(MIT_LICENSE_BLOB_LINES_GO)
if current_header == mit_header:
lines = lines[len(MIT_LICENSE_BLOB_LINES_GO)+1:]
found = False
changed = False
for i, line in enumerate(lines[:5]):
m = COPYRIGHT_RE.search(line)
if not m:
continue
found = True
year = int(m.group(1))
if year == CURRENT_YEAR:
break
new_line = COPYRIGHT_RE.sub('Copyright (c) %d' % CURRENT_YEAR, line)
assert line != new_line, ('Could not change year in: %s' % line)
lines[i] = new_line
changed = True
break
if not found:
if 'Code generated by' in lines[0]:
lines[1:1] = ['\n'] + LICENSE_BLOB_LINES_GO
else:
lines[0:0] = LICENSE_BLOB_LINES_GO + ['\n']
changed = True
if changed:
with open(name, 'w') as f:
for line in lines:
f.write(line)
print(name)
def main():
if len(sys.argv) == 1:
print('USAGE: %s FILE ...' % sys.argv[0])
sys.exit(1)
for name in sys.argv[1:]:
if name.endswith('.go'):
try:
update_go_license(name)
except Exception as error:
logger.error('Failed to process file %s', name)
logger.exception(error)
raise error
else:
raise NotImplementedError('Unsupported file type: %s' % name)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e
python scripts/updateLicense.py $(git ls-files "*\.go" | grep -v thrift-gen | grep -v tracetest)

View File

@ -0,0 +1,242 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"sync"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
)
// Span implements opentracing.Span
type Span struct {
sync.RWMutex
tracer *Tracer
context SpanContext
// The name of the "operation" this span is an instance of.
// Known as a "span name" in some implementations.
operationName string
// firstInProcess, if true, indicates that this span is the root of the (sub)tree
// of spans in the current process. In other words it's true for the root spans,
// and the ingress spans when the process joins another trace.
firstInProcess bool
// startTime is the timestamp indicating when the span began, with microseconds precision.
startTime time.Time
// duration returns duration of the span with microseconds precision.
// Zero value means duration is unknown.
duration time.Duration
// tags attached to this span
tags []Tag
// The span's "micro-log"
logs []opentracing.LogRecord
// references for this span
references []Reference
observer ContribSpanObserver
}
// Tag is a simple key value wrapper.
// TODO deprecate in the next major release, use opentracing.Tag instead.
type Tag struct {
key string
value interface{}
}
// SetOperationName sets or changes the operation name.
func (s *Span) SetOperationName(operationName string) opentracing.Span {
s.Lock()
defer s.Unlock()
if s.context.IsSampled() {
s.operationName = operationName
}
s.observer.OnSetOperationName(operationName)
return s
}
// SetTag implements SetTag() of opentracing.Span
func (s *Span) SetTag(key string, value interface{}) opentracing.Span {
s.observer.OnSetTag(key, value)
if key == string(ext.SamplingPriority) && setSamplingPriority(s, value) {
return s
}
s.Lock()
defer s.Unlock()
if s.context.IsSampled() {
s.setTagNoLocking(key, value)
}
return s
}
func (s *Span) setTagNoLocking(key string, value interface{}) {
s.tags = append(s.tags, Tag{key: key, value: value})
}
// LogFields implements opentracing.Span API
func (s *Span) LogFields(fields ...log.Field) {
s.Lock()
defer s.Unlock()
if !s.context.IsSampled() {
return
}
s.logFieldsNoLocking(fields...)
}
// this function should only be called while holding a Write lock
func (s *Span) logFieldsNoLocking(fields ...log.Field) {
lr := opentracing.LogRecord{
Fields: fields,
Timestamp: time.Now(),
}
s.appendLog(lr)
}
// LogKV implements opentracing.Span API
func (s *Span) LogKV(alternatingKeyValues ...interface{}) {
s.RLock()
sampled := s.context.IsSampled()
s.RUnlock()
if !sampled {
return
}
fields, err := log.InterleavedKVToFields(alternatingKeyValues...)
if err != nil {
s.LogFields(log.Error(err), log.String("function", "LogKV"))
return
}
s.LogFields(fields...)
}
// LogEvent implements opentracing.Span API
func (s *Span) LogEvent(event string) {
s.Log(opentracing.LogData{Event: event})
}
// LogEventWithPayload implements opentracing.Span API
func (s *Span) LogEventWithPayload(event string, payload interface{}) {
s.Log(opentracing.LogData{Event: event, Payload: payload})
}
// Log implements opentracing.Span API
func (s *Span) Log(ld opentracing.LogData) {
s.Lock()
defer s.Unlock()
if s.context.IsSampled() {
if ld.Timestamp.IsZero() {
ld.Timestamp = s.tracer.timeNow()
}
s.appendLog(ld.ToLogRecord())
}
}
// this function should only be called while holding a Write lock
func (s *Span) appendLog(lr opentracing.LogRecord) {
// TODO add logic to limit number of logs per span (issue #46)
s.logs = append(s.logs, lr)
}
// SetBaggageItem implements SetBaggageItem() of opentracing.SpanContext
func (s *Span) SetBaggageItem(key, value string) opentracing.Span {
s.Lock()
defer s.Unlock()
s.tracer.setBaggage(s, key, value)
return s
}
// BaggageItem implements BaggageItem() of opentracing.SpanContext
func (s *Span) BaggageItem(key string) string {
s.RLock()
defer s.RUnlock()
return s.context.baggage[key]
}
// Finish implements opentracing.Span API
func (s *Span) Finish() {
s.FinishWithOptions(opentracing.FinishOptions{})
}
// FinishWithOptions implements opentracing.Span API
func (s *Span) FinishWithOptions(options opentracing.FinishOptions) {
if options.FinishTime.IsZero() {
options.FinishTime = s.tracer.timeNow()
}
s.observer.OnFinish(options)
s.Lock()
if s.context.IsSampled() {
s.duration = options.FinishTime.Sub(s.startTime)
// Note: bulk logs are not subject to maxLogsPerSpan limit
if options.LogRecords != nil {
s.logs = append(s.logs, options.LogRecords...)
}
for _, ld := range options.BulkLogData {
s.logs = append(s.logs, ld.ToLogRecord())
}
}
s.Unlock()
// call reportSpan even for non-sampled traces, to return span to the pool
s.tracer.reportSpan(s)
}
// Context implements opentracing.Span API
func (s *Span) Context() opentracing.SpanContext {
return s.context
}
// Tracer implements opentracing.Span API
func (s *Span) Tracer() opentracing.Tracer {
return s.tracer
}
func (s *Span) String() string {
s.RLock()
defer s.RUnlock()
return s.context.String()
}
// OperationName allows retrieving current operation name.
func (s *Span) OperationName() string {
s.RLock()
defer s.RUnlock()
return s.operationName
}
func (s *Span) serviceName() string {
return s.tracer.serviceName
}
func setSamplingPriority(s *Span, value interface{}) bool {
s.Lock()
defer s.Unlock()
if val, ok := value.(uint16); ok {
if val > 0 {
s.context.flags = s.context.flags | flagDebug | flagSampled
} else {
s.context.flags = s.context.flags & (^flagSampled)
}
return true
}
return false
}

View File

@ -0,0 +1,90 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jaeger
import (
"testing"
"github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBaggageIterator(t *testing.T) {
service := "DOOP"
tracer, closer := NewTracer(service, NewConstSampler(true), NewNullReporter())
defer closer.Close()
sp1 := tracer.StartSpan("s1").(*Span)
sp1.SetBaggageItem("Some_Key", "12345")
sp1.SetBaggageItem("Some-other-key", "42")
expectedBaggage := map[string]string{"Some_Key": "12345", "Some-other-key": "42"}
assertBaggage(t, sp1, expectedBaggage)
assertBaggageRecords(t, sp1, expectedBaggage)
b := extractBaggage(sp1, false) // break out early
assert.Equal(t, 1, len(b), "only one baggage item should be extracted")
sp2 := tracer.StartSpan("s2", opentracing.ChildOf(sp1.Context())).(*Span)
assertBaggage(t, sp2, expectedBaggage) // child inherits the same baggage
require.Len(t, sp2.logs, 0) // child doesn't inherit the baggage logs
}
func assertBaggageRecords(t *testing.T, sp *Span, expected map[string]string) {
require.Len(t, sp.logs, len(expected))
for _, logRecord := range sp.logs {
require.Len(t, logRecord.Fields, 3)
require.Equal(t, "event:baggage", logRecord.Fields[0].String())
key := logRecord.Fields[1].Value().(string)
value := logRecord.Fields[2].Value().(string)
require.Contains(t, expected, key)
assert.Equal(t, expected[key], value)
}
}
func assertBaggage(t *testing.T, sp opentracing.Span, expected map[string]string) {
b := extractBaggage(sp, true)
assert.Equal(t, expected, b)
}
func extractBaggage(sp opentracing.Span, allItems bool) map[string]string {
b := make(map[string]string)
sp.Context().ForeachBaggageItem(func(k, v string) bool {
b[k] = v
return allItems
})
return b
}
func TestSpanProperties(t *testing.T) {
tracer, closer := NewTracer("DOOP", NewConstSampler(true), NewNullReporter())
defer closer.Close()
sp1 := tracer.StartSpan("s1").(*Span)
assert.Equal(t, tracer, sp1.Tracer())
assert.NotNil(t, sp1.Context())
}
func TestSpanOperationName(t *testing.T) {
tracer, closer := NewTracer("DOOP", NewConstSampler(true), NewNullReporter())
defer closer.Close()
sp1 := tracer.StartSpan("s1").(*Span)
sp1.SetOperationName("s2")
sp1.Finish()
assert.Equal(t, "s2", sp1.OperationName())
}

View File

@ -0,0 +1,189 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutils
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"github.com/apache/thrift/lib/go/thrift"
"github.com/uber/jaeger-client-go/thrift-gen/agent"
"github.com/uber/jaeger-client-go/thrift-gen/jaeger"
"github.com/uber/jaeger-client-go/thrift-gen/sampling"
"github.com/uber/jaeger-client-go/thrift-gen/zipkincore"
"github.com/uber/jaeger-client-go/utils"
)
// StartMockAgent runs a mock representation of jaeger-agent.
// This function returns a started server.
func StartMockAgent() (*MockAgent, error) {
transport, err := NewTUDPServerTransport("127.0.0.1:0")
if err != nil {
return nil, err
}
samplingManager := newSamplingManager()
samplingHandler := &samplingHandler{manager: samplingManager}
samplingServer := httptest.NewServer(samplingHandler)
agent := &MockAgent{
transport: transport,
samplingMgr: samplingManager,
samplingSrv: samplingServer,
}
var started sync.WaitGroup
started.Add(1)
go agent.serve(&started)
started.Wait()
return agent, nil
}
// Close stops the serving of traffic
func (s *MockAgent) Close() {
atomic.StoreUint32(&s.serving, 0)
s.transport.Close()
s.samplingSrv.Close()
}
// MockAgent is a mock representation of Jaeger Agent.
// It receives spans over UDP, and has an HTTP endpoint for sampling strategies.
type MockAgent struct {
transport *TUDPTransport
jaegerBatches []*jaeger.Batch
mutex sync.Mutex
serving uint32
samplingMgr *samplingManager
samplingSrv *httptest.Server
}
// SpanServerAddr returns the UDP host:port where MockAgent listens for spans
func (s *MockAgent) SpanServerAddr() string {
return s.transport.Addr().String()
}
// SpanServerClient returns a UDP client that can be used to send spans to the MockAgent
func (s *MockAgent) SpanServerClient() (agent.Agent, error) {
return utils.NewAgentClientUDP(s.SpanServerAddr(), 0)
}
// SamplingServerAddr returns the host:port of HTTP server exposing sampling strategy endpoint
func (s *MockAgent) SamplingServerAddr() string {
return s.samplingSrv.Listener.Addr().String()
}
func (s *MockAgent) serve(started *sync.WaitGroup) {
handler := agent.NewAgentProcessor(s)
protocolFact := thrift.NewTCompactProtocolFactory()
buf := make([]byte, utils.UDPPacketMaxLength, utils.UDPPacketMaxLength)
trans := thrift.NewTMemoryBufferLen(utils.UDPPacketMaxLength)
atomic.StoreUint32(&s.serving, 1)
started.Done()
for s.IsServing() {
n, err := s.transport.Read(buf)
if err == nil {
trans.Write(buf[:n])
protocol := protocolFact.GetProtocol(trans)
handler.Process(protocol, protocol)
}
}
}
// EmitZipkinBatch is deprecated, use EmitBatch
func (s *MockAgent) EmitZipkinBatch(spans []*zipkincore.Span) (err error) {
// TODO remove this for 3.0.0
return errors.New("Not implemented")
}
// GetZipkinSpans is deprecated use GetJaegerBatches
func (s *MockAgent) GetZipkinSpans() []*zipkincore.Span {
return nil
}
// ResetZipkinSpans is deprecated use ResetJaegerBatches
func (s *MockAgent) ResetZipkinSpans() {}
// EmitBatch implements EmitBatch() of TChanSamplingManagerServer
func (s *MockAgent) EmitBatch(batch *jaeger.Batch) (err error) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.jaegerBatches = append(s.jaegerBatches, batch)
return err
}
// IsServing indicates whether the server is currently serving traffic
func (s *MockAgent) IsServing() bool {
return atomic.LoadUint32(&s.serving) == 1
}
// AddSamplingStrategy registers a sampling strategy for a service
func (s *MockAgent) AddSamplingStrategy(service string, strategy *sampling.SamplingStrategyResponse) {
s.samplingMgr.AddSamplingStrategy(service, strategy)
}
// GetJaegerBatches returns accumulated Jaeger batches
func (s *MockAgent) GetJaegerBatches() []*jaeger.Batch {
s.mutex.Lock()
defer s.mutex.Unlock()
n := len(s.jaegerBatches)
batches := make([]*jaeger.Batch, n, n)
copy(batches, s.jaegerBatches)
return batches
}
// ResetJaegerBatches discards accumulated Jaeger batches
func (s *MockAgent) ResetJaegerBatches() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.jaegerBatches = nil
}
type samplingHandler struct {
manager *samplingManager
}
func (h *samplingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
services := r.URL.Query()["service"]
if len(services) == 0 {
http.Error(w, "'service' parameter is empty", http.StatusBadRequest)
return
}
if len(services) > 1 {
http.Error(w, "'service' parameter must occur only once", http.StatusBadRequest)
return
}
resp, err := h.manager.GetSamplingStrategy(services[0])
if err != nil {
http.Error(w, fmt.Sprintf("Error retrieving strategy: %+v", err), http.StatusInternalServerError)
return
}
json, err := json.Marshal(resp)
if err != nil {
http.Error(w, "Cannot marshall Thrift to JSON", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
if _, err := w.Write(json); err != nil {
return
}
}

View File

@ -0,0 +1,93 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutils
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/jaeger-client-go/thrift-gen/jaeger"
"github.com/uber/jaeger-client-go/thrift-gen/sampling"
"github.com/uber/jaeger-client-go/utils"
)
func TestMockAgentSpanServer(t *testing.T) {
mockAgent, err := StartMockAgent()
require.NoError(t, err)
defer mockAgent.Close()
client, err := mockAgent.SpanServerClient()
require.NoError(t, err)
for i := 1; i < 5; i++ {
batch := &jaeger.Batch{Process: &jaeger.Process{ServiceName: "svc"}}
spans := make([]*jaeger.Span, i, i)
for j := 0; j < i; j++ {
spans[j] = jaeger.NewSpan()
spans[j].OperationName = fmt.Sprintf("span-%d", j)
}
batch.Spans = spans
err = client.EmitBatch(batch)
assert.NoError(t, err)
for k := 0; k < 100; k++ {
time.Sleep(time.Millisecond)
batches := mockAgent.GetJaegerBatches()
if len(batches) > 0 && len(batches[0].Spans) == i {
break
}
}
batches := mockAgent.GetJaegerBatches()
require.NotEmpty(t, len(batches))
require.Equal(t, i, len(batches[0].Spans))
for j := 0; j < i; j++ {
assert.Equal(t, fmt.Sprintf("span-%d", j), batches[0].Spans[j].OperationName)
}
mockAgent.ResetJaegerBatches()
}
}
func TestMockAgentSamplingManager(t *testing.T) {
mockAgent, err := StartMockAgent()
require.NoError(t, err)
defer mockAgent.Close()
err = utils.GetJSON("http://"+mockAgent.SamplingServerAddr()+"/", nil)
require.Error(t, err, "no 'service' parameter")
err = utils.GetJSON("http://"+mockAgent.SamplingServerAddr()+"/?service=a&service=b", nil)
require.Error(t, err, "Too many 'service' parameters")
var resp sampling.SamplingStrategyResponse
err = utils.GetJSON("http://"+mockAgent.SamplingServerAddr()+"/?service=something", &resp)
require.NoError(t, err)
assert.Equal(t, sampling.SamplingStrategyType_PROBABILISTIC, resp.StrategyType)
mockAgent.AddSamplingStrategy("service123", &sampling.SamplingStrategyResponse{
StrategyType: sampling.SamplingStrategyType_RATE_LIMITING,
RateLimitingSampling: &sampling.RateLimitingSamplingStrategy{
MaxTracesPerSecond: 123,
},
})
err = utils.GetJSON("http://"+mockAgent.SamplingServerAddr()+"/?service=service123", &resp)
require.NoError(t, err)
assert.Equal(t, sampling.SamplingStrategyType_RATE_LIMITING, resp.StrategyType)
require.NotNil(t, resp.RateLimitingSampling)
assert.EqualValues(t, 123, resp.RateLimitingSampling.MaxTracesPerSecond)
}

View File

@ -0,0 +1,53 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutils
import (
"sync"
"github.com/uber/jaeger-client-go/thrift-gen/sampling"
)
func newSamplingManager() *samplingManager {
return &samplingManager{
sampling: make(map[string]*sampling.SamplingStrategyResponse),
}
}
type samplingManager struct {
sampling map[string]*sampling.SamplingStrategyResponse
mutex sync.Mutex
}
// GetSamplingStrategy implements handler method of sampling.SamplingManager
func (s *samplingManager) GetSamplingStrategy(serviceName string) (*sampling.SamplingStrategyResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if strategy, ok := s.sampling[serviceName]; ok {
return strategy, nil
}
return &sampling.SamplingStrategyResponse{
StrategyType: sampling.SamplingStrategyType_PROBABILISTIC,
ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{
SamplingRate: 0.01,
}}, nil
}
// AddSamplingStrategy registers a sampling strategy for a service
func (s *samplingManager) AddSamplingStrategy(service string, strategy *sampling.SamplingStrategyResponse) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.sampling[service] = strategy
}

View File

@ -0,0 +1,106 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutils
import (
"bytes"
"net"
"sync/atomic"
"github.com/apache/thrift/lib/go/thrift"
)
const (
// used in RemainingBytes()
maxRemainingBytes = ^uint64(0)
)
// TUDPTransport does UDP as a thrift.TTransport (read-only, write functions not implemented).
type TUDPTransport struct {
conn *net.UDPConn
addr net.Addr
writeBuf bytes.Buffer
closed uint32
}
// NewTUDPServerTransport creates a net.UDPConn-backed TTransport for Thrift servers
// It will listen for incoming udp packets on the specified host/port
// Example:
// trans, err := utils.NewTUDPClientTransport("localhost:9001")
func NewTUDPServerTransport(hostPort string) (*TUDPTransport, error) {
addr, err := net.ResolveUDPAddr("udp", hostPort)
if err != nil {
return nil, thrift.NewTTransportException(thrift.NOT_OPEN, err.Error())
}
conn, err := net.ListenUDP(addr.Network(), addr)
if err != nil {
return nil, thrift.NewTTransportException(thrift.NOT_OPEN, err.Error())
}
return &TUDPTransport{addr: conn.LocalAddr(), conn: conn}, nil
}
// Open does nothing as connection is opened on creation
// Required to maintain thrift.TTransport interface
func (p *TUDPTransport) Open() error {
return nil
}
// Conn retrieves the underlying net.UDPConn
func (p *TUDPTransport) Conn() *net.UDPConn {
return p.conn
}
// IsOpen returns true if the connection is open
func (p *TUDPTransport) IsOpen() bool {
return p.conn != nil && atomic.LoadUint32(&p.closed) == 0
}
// Close closes the connection
func (p *TUDPTransport) Close() error {
if p.conn != nil && atomic.CompareAndSwapUint32(&p.closed, 0, 1) {
return p.conn.Close()
}
return nil
}
// Addr returns the address that the transport is listening on or writing to
func (p *TUDPTransport) Addr() net.Addr {
return p.addr
}
// Read reads one UDP packet and puts it in the specified buf
func (p *TUDPTransport) Read(buf []byte) (int, error) {
if !p.IsOpen() {
return 0, thrift.NewTTransportException(thrift.NOT_OPEN, "Connection not open")
}
n, err := p.conn.Read(buf)
return n, thrift.NewTTransportExceptionFromError(err)
}
// RemainingBytes returns the max number of bytes (same as Thrift's StreamTransport) as we
// do not know how many bytes we have left.
func (p *TUDPTransport) RemainingBytes() uint64 {
return maxRemainingBytes
}
// Write writes specified buf to the write buffer
func (p *TUDPTransport) Write(buf []byte) (int, error) {
return 0, thrift.NewTTransportException(thrift.UNKNOWN_TRANSPORT_EXCEPTION, "Write not implemented")
}
// Flush flushes the write buffer as one udp packet
func (p *TUDPTransport) Flush() error {
return thrift.NewTTransportException(thrift.UNKNOWN_TRANSPORT_EXCEPTION, "Flush not implemented")
}

View File

@ -0,0 +1,67 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testutils
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUDPTransport(t *testing.T) {
server, err := NewTUDPServerTransport("127.0.0.1:0")
require.NoError(t, err)
defer server.Close()
assert.NoError(t, server.Open())
assert.True(t, server.IsOpen())
assert.NotNil(t, server.Conn())
c := make(chan []byte)
defer close(c)
go serveOnce(t, server, c)
destAddr, err := net.ResolveUDPAddr("udp", server.Addr().String())
require.NoError(t, err)
connUDP, err := net.DialUDP(destAddr.Network(), nil, destAddr)
require.NoError(t, err)
defer connUDP.Close()
n, err := connUDP.Write([]byte("test"))
assert.NoError(t, err)
assert.Equal(t, 4, n)
select {
case data := <-c:
assert.Equal(t, "test", string(data))
case <-time.After(time.Second * 1):
t.Error("Server did not respond in time")
}
}
func serveOnce(t *testing.T, transport *TUDPTransport, c chan []byte) {
b := make([]byte, 65000, 65000)
n, err := transport.Read(b)
if err == nil {
c <- b[:n]
} else {
panic("Server failed to read: " + err.Error())
}
}

View File

@ -0,0 +1,410 @@
// Autogenerated by Thrift Compiler (0.9.3)
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
package agent
import (
"bytes"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"github.com/uber/jaeger-client-go/thrift-gen/jaeger"
"github.com/uber/jaeger-client-go/thrift-gen/zipkincore"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = bytes.Equal
var _ = zipkincore.GoUnusedProtection__
type Agent interface {
// Parameters:
// - Spans
EmitZipkinBatch(spans []*zipkincore.Span) (err error)
// Parameters:
// - Batch
EmitBatch(batch *jaeger.Batch) (err error)
}
type AgentClient struct {
Transport thrift.TTransport
ProtocolFactory thrift.TProtocolFactory
InputProtocol thrift.TProtocol
OutputProtocol thrift.TProtocol
SeqId int32
}
func NewAgentClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *AgentClient {
return &AgentClient{Transport: t,
ProtocolFactory: f,
InputProtocol: f.GetProtocol(t),
OutputProtocol: f.GetProtocol(t),
SeqId: 0,
}
}
func NewAgentClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *AgentClient {
return &AgentClient{Transport: t,
ProtocolFactory: nil,
InputProtocol: iprot,
OutputProtocol: oprot,
SeqId: 0,
}
}
// Parameters:
// - Spans
func (p *AgentClient) EmitZipkinBatch(spans []*zipkincore.Span) (err error) {
if err = p.sendEmitZipkinBatch(spans); err != nil {
return
}
return
}
func (p *AgentClient) sendEmitZipkinBatch(spans []*zipkincore.Span) (err error) {
oprot := p.OutputProtocol
if oprot == nil {
oprot = p.ProtocolFactory.GetProtocol(p.Transport)
p.OutputProtocol = oprot
}
p.SeqId++
if err = oprot.WriteMessageBegin("emitZipkinBatch", thrift.ONEWAY, p.SeqId); err != nil {
return
}
args := AgentEmitZipkinBatchArgs{
Spans: spans,
}
if err = args.Write(oprot); err != nil {
return
}
if err = oprot.WriteMessageEnd(); err != nil {
return
}
return oprot.Flush()
}
// Parameters:
// - Batch
func (p *AgentClient) EmitBatch(batch *jaeger.Batch) (err error) {
if err = p.sendEmitBatch(batch); err != nil {
return
}
return
}
func (p *AgentClient) sendEmitBatch(batch *jaeger.Batch) (err error) {
oprot := p.OutputProtocol
if oprot == nil {
oprot = p.ProtocolFactory.GetProtocol(p.Transport)
p.OutputProtocol = oprot
}
p.SeqId++
if err = oprot.WriteMessageBegin("emitBatch", thrift.ONEWAY, p.SeqId); err != nil {
return
}
args := AgentEmitBatchArgs{
Batch: batch,
}
if err = args.Write(oprot); err != nil {
return
}
if err = oprot.WriteMessageEnd(); err != nil {
return
}
return oprot.Flush()
}
type AgentProcessor struct {
processorMap map[string]thrift.TProcessorFunction
handler Agent
}
func (p *AgentProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) {
p.processorMap[key] = processor
}
func (p *AgentProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) {
processor, ok = p.processorMap[key]
return processor, ok
}
func (p *AgentProcessor) ProcessorMap() map[string]thrift.TProcessorFunction {
return p.processorMap
}
func NewAgentProcessor(handler Agent) *AgentProcessor {
self0 := &AgentProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)}
self0.processorMap["emitZipkinBatch"] = &agentProcessorEmitZipkinBatch{handler: handler}
self0.processorMap["emitBatch"] = &agentProcessorEmitBatch{handler: handler}
return self0
}
func (p *AgentProcessor) Process(iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) {
name, _, seqId, err := iprot.ReadMessageBegin()
if err != nil {
return false, err
}
if processor, ok := p.GetProcessorFunction(name); ok {
return processor.Process(seqId, iprot, oprot)
}
iprot.Skip(thrift.STRUCT)
iprot.ReadMessageEnd()
x1 := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name)
oprot.WriteMessageBegin(name, thrift.EXCEPTION, seqId)
x1.Write(oprot)
oprot.WriteMessageEnd()
oprot.Flush()
return false, x1
}
type agentProcessorEmitZipkinBatch struct {
handler Agent
}
func (p *agentProcessorEmitZipkinBatch) Process(seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) {
args := AgentEmitZipkinBatchArgs{}
if err = args.Read(iprot); err != nil {
iprot.ReadMessageEnd()
return false, err
}
iprot.ReadMessageEnd()
var err2 error
if err2 = p.handler.EmitZipkinBatch(args.Spans); err2 != nil {
return true, err2
}
return true, nil
}
type agentProcessorEmitBatch struct {
handler Agent
}
func (p *agentProcessorEmitBatch) Process(seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) {
args := AgentEmitBatchArgs{}
if err = args.Read(iprot); err != nil {
iprot.ReadMessageEnd()
return false, err
}
iprot.ReadMessageEnd()
var err2 error
if err2 = p.handler.EmitBatch(args.Batch); err2 != nil {
return true, err2
}
return true, nil
}
// HELPER FUNCTIONS AND STRUCTURES
// Attributes:
// - Spans
type AgentEmitZipkinBatchArgs struct {
Spans []*zipkincore.Span `thrift:"spans,1" json:"spans"`
}
func NewAgentEmitZipkinBatchArgs() *AgentEmitZipkinBatchArgs {
return &AgentEmitZipkinBatchArgs{}
}
func (p *AgentEmitZipkinBatchArgs) GetSpans() []*zipkincore.Span {
return p.Spans
}
func (p *AgentEmitZipkinBatchArgs) Read(iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin()
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if err := p.readField1(iprot); err != nil {
return err
}
default:
if err := iprot.Skip(fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *AgentEmitZipkinBatchArgs) readField1(iprot thrift.TProtocol) error {
_, size, err := iprot.ReadListBegin()
if err != nil {
return thrift.PrependError("error reading list begin: ", err)
}
tSlice := make([]*zipkincore.Span, 0, size)
p.Spans = tSlice
for i := 0; i < size; i++ {
_elem2 := &zipkincore.Span{}
if err := _elem2.Read(iprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", _elem2), err)
}
p.Spans = append(p.Spans, _elem2)
}
if err := iprot.ReadListEnd(); err != nil {
return thrift.PrependError("error reading list end: ", err)
}
return nil
}
func (p *AgentEmitZipkinBatchArgs) Write(oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin("emitZipkinBatch_args"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if err := p.writeField1(oprot); err != nil {
return err
}
if err := oprot.WriteFieldStop(); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *AgentEmitZipkinBatchArgs) writeField1(oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin("spans", thrift.LIST, 1); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:spans: ", p), err)
}
if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Spans)); err != nil {
return thrift.PrependError("error writing list begin: ", err)
}
for _, v := range p.Spans {
if err := v.Write(oprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", v), err)
}
}
if err := oprot.WriteListEnd(); err != nil {
return thrift.PrependError("error writing list end: ", err)
}
if err := oprot.WriteFieldEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 1:spans: ", p), err)
}
return err
}
func (p *AgentEmitZipkinBatchArgs) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("AgentEmitZipkinBatchArgs(%+v)", *p)
}
// Attributes:
// - Batch
type AgentEmitBatchArgs struct {
Batch *jaeger.Batch `thrift:"batch,1" json:"batch"`
}
func NewAgentEmitBatchArgs() *AgentEmitBatchArgs {
return &AgentEmitBatchArgs{}
}
var AgentEmitBatchArgs_Batch_DEFAULT *jaeger.Batch
func (p *AgentEmitBatchArgs) GetBatch() *jaeger.Batch {
if !p.IsSetBatch() {
return AgentEmitBatchArgs_Batch_DEFAULT
}
return p.Batch
}
func (p *AgentEmitBatchArgs) IsSetBatch() bool {
return p.Batch != nil
}
func (p *AgentEmitBatchArgs) Read(iprot thrift.TProtocol) error {
if _, err := iprot.ReadStructBegin(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err)
}
for {
_, fieldTypeId, fieldId, err := iprot.ReadFieldBegin()
if err != nil {
return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err)
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if err := p.readField1(iprot); err != nil {
return err
}
default:
if err := iprot.Skip(fieldTypeId); err != nil {
return err
}
}
if err := iprot.ReadFieldEnd(); err != nil {
return err
}
}
if err := iprot.ReadStructEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
return nil
}
func (p *AgentEmitBatchArgs) readField1(iprot thrift.TProtocol) error {
p.Batch = &jaeger.Batch{}
if err := p.Batch.Read(iprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Batch), err)
}
return nil
}
func (p *AgentEmitBatchArgs) Write(oprot thrift.TProtocol) error {
if err := oprot.WriteStructBegin("emitBatch_args"); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
}
if err := p.writeField1(oprot); err != nil {
return err
}
if err := oprot.WriteFieldStop(); err != nil {
return thrift.PrependError("write field stop error: ", err)
}
if err := oprot.WriteStructEnd(); err != nil {
return thrift.PrependError("write struct stop error: ", err)
}
return nil
}
func (p *AgentEmitBatchArgs) writeField1(oprot thrift.TProtocol) (err error) {
if err := oprot.WriteFieldBegin("batch", thrift.STRUCT, 1); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:batch: ", p), err)
}
if err := p.Batch.Write(oprot); err != nil {
return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Batch), err)
}
if err := oprot.WriteFieldEnd(); err != nil {
return thrift.PrependError(fmt.Sprintf("%T write field end error 1:batch: ", p), err)
}
return err
}
func (p *AgentEmitBatchArgs) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("AgentEmitBatchArgs(%+v)", *p)
}

View File

@ -0,0 +1,21 @@
// Autogenerated by Thrift Compiler (0.9.3)
// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
package agent
import (
"bytes"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"github.com/uber/jaeger-client-go/thrift-gen/zipkincore"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = bytes.Equal
var _ = zipkincore.GoUnusedProtection__
func init() {
}

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