From 1465fd513cdcc24d2259fe0177488aa6bf061435 Mon Sep 17 00:00:00 2001 From: Yuri Shkuro Date: Sat, 18 Mar 2017 23:48:50 -0400 Subject: [PATCH] Migrate jaeger-agent implementation from internal repository (#61) --- .gitignore | 1 + Makefile | 2 +- cmd/agent/README.md | 35 + cmd/agent/app/agent.go | 83 ++ cmd/agent/app/agent_test.go | 93 +++ cmd/agent/app/config.go | 299 +++++++ cmd/agent/app/config_test.go | 159 ++++ .../buffered_read_transport.go | 79 ++ .../buffered_read_transport_test.go | 74 ++ cmd/agent/app/flags.go | 66 ++ cmd/agent/app/flags_test.go | 51 ++ cmd/agent/app/processors/processor.go | 27 + cmd/agent/app/processors/thrift_processor.go | 123 +++ .../app/processors/thrift_processor_test.go | 237 ++++++ cmd/agent/app/reporter/reporter.go | 67 ++ cmd/agent/app/reporter/reporter_test.go | 81 ++ cmd/agent/app/reporter/tcollector_reporter.go | 125 +++ .../app/reporter/tcollector_reporter_test.go | 128 +++ cmd/agent/app/sampling/manager.go | 30 + cmd/agent/app/sampling/server.go | 144 ++++ cmd/agent/app/sampling/server_test.go | 213 +++++ cmd/agent/app/sampling/tcollector_proxy.go | 64 ++ .../app/sampling/tcollector_proxy_test.go | 79 ++ .../app/sampling/thrift-0.9.2/constants.go | 38 + cmd/agent/app/sampling/thrift-0.9.2/ttypes.go | 768 ++++++++++++++++++ cmd/agent/app/servers/server.go | 55 ++ cmd/agent/app/servers/server_test.go | 44 + cmd/agent/app/servers/tbuffered_server.go | 133 +++ .../app/servers/tbuffered_server_test.go | 118 +++ cmd/agent/app/servers/thriftudp/transport.go | 157 ++++ .../app/servers/thriftudp/transport_test.go | 231 ++++++ cmd/agent/app/testdata/test_config.yaml | 25 + cmd/agent/app/testutils/fixture.go | 37 + cmd/agent/app/testutils/in_memory_reporter.go | 73 ++ .../app/testutils/in_memory_reporter_test.go | 46 ++ cmd/agent/app/testutils/mock_collector.go | 152 ++++ .../app/testutils/mock_collector_test.go | 141 ++++ .../app/testutils/mock_sampling_manager.go | 64 ++ cmd/agent/app/testutils/thriftudp_client.go | 54 ++ .../app/testutils/thriftudp_client_test.go | 49 ++ cmd/agent/main.go | 34 +- glide.lock | 82 +- glide.yaml | 1 - 43 files changed, 4501 insertions(+), 61 deletions(-) create mode 100644 cmd/agent/README.md create mode 100644 cmd/agent/app/agent.go create mode 100644 cmd/agent/app/agent_test.go create mode 100644 cmd/agent/app/config.go create mode 100644 cmd/agent/app/config_test.go create mode 100644 cmd/agent/app/customtransports/buffered_read_transport.go create mode 100644 cmd/agent/app/customtransports/buffered_read_transport_test.go create mode 100644 cmd/agent/app/flags.go create mode 100644 cmd/agent/app/flags_test.go create mode 100644 cmd/agent/app/processors/processor.go create mode 100644 cmd/agent/app/processors/thrift_processor.go create mode 100644 cmd/agent/app/processors/thrift_processor_test.go create mode 100644 cmd/agent/app/reporter/reporter.go create mode 100644 cmd/agent/app/reporter/reporter_test.go create mode 100644 cmd/agent/app/reporter/tcollector_reporter.go create mode 100644 cmd/agent/app/reporter/tcollector_reporter_test.go create mode 100644 cmd/agent/app/sampling/manager.go create mode 100644 cmd/agent/app/sampling/server.go create mode 100644 cmd/agent/app/sampling/server_test.go create mode 100644 cmd/agent/app/sampling/tcollector_proxy.go create mode 100644 cmd/agent/app/sampling/tcollector_proxy_test.go create mode 100644 cmd/agent/app/sampling/thrift-0.9.2/constants.go create mode 100644 cmd/agent/app/sampling/thrift-0.9.2/ttypes.go create mode 100644 cmd/agent/app/servers/server.go create mode 100644 cmd/agent/app/servers/server_test.go create mode 100644 cmd/agent/app/servers/tbuffered_server.go create mode 100644 cmd/agent/app/servers/tbuffered_server_test.go create mode 100644 cmd/agent/app/servers/thriftudp/transport.go create mode 100644 cmd/agent/app/servers/thriftudp/transport_test.go create mode 100644 cmd/agent/app/testdata/test_config.yaml create mode 100644 cmd/agent/app/testutils/fixture.go create mode 100644 cmd/agent/app/testutils/in_memory_reporter.go create mode 100644 cmd/agent/app/testutils/in_memory_reporter_test.go create mode 100644 cmd/agent/app/testutils/mock_collector.go create mode 100644 cmd/agent/app/testutils/mock_collector_test.go create mode 100644 cmd/agent/app/testutils/mock_sampling_manager.go create mode 100644 cmd/agent/app/testutils/thriftudp_client.go create mode 100644 cmd/agent/app/testutils/thriftudp_client_test.go diff --git a/.gitignore b/.gitignore index 698f0e20498..177ff1696c4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ _site/ env/ Gemfile.lock vendor/ +examples/hotrod/hotrod diff --git a/Makefile b/Makefile index c3d512e0535..e50c4c6b73b 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ fmt: lint: $(GOVET) $(PACKAGES) @cat /dev/null > $(LINT_LOG) - @$(foreach pkg, $(PACKAGES), $(GOLINT) $(pkg) | grep -v thrift-gen >> $(LINT_LOG) || true;) + @$(foreach pkg, $(PACKAGES), $(GOLINT) $(pkg) | grep -v -e thrift-gen -e thrift-0.9.2 >> $(LINT_LOG) || true;) @[ ! -s "$(LINT_LOG)" ] || (echo "Lint Failures" | cat - $(LINT_LOG) && false) @$(GOFMT) -e -s -l $(ALL_SRC) > $(FMT_LOG) @./scripts/updateLicenses.sh >> $(FMT_LOG) diff --git a/cmd/agent/README.md b/cmd/agent/README.md new file mode 100644 index 00000000000..2867af2e256 --- /dev/null +++ b/cmd/agent/README.md @@ -0,0 +1,35 @@ +# Jaeger Agent + +`jaeger-agent` is a daemon program that runs on every host and receives +tracing information submitted by applications via Jaeger client +libraries. + +## Structure + +* Agent + * processor as ThriftProcessor + * server as TBufferedServer + * Thrift UDP Transport + * reporter as TCollectorReporter + * sampling server + * sampling manager as sampling.TCollectorProxy + +### UDP Server + +Listens on UDP transport, reads data as `[]byte` and forwards to +`processor` over channel. Processor has N workers that read from +the channel, convert to thrift-generated object model, and pass on +to the Reporter. `TCollectorReporter` submits the spans to remote +`tcollector` service. + +### Sampling Server + +An HTTP server handling request in the form + + http://localhost:port/sampling?service=xxxx` + +Delegates to `sampling.Manager` to get the sampling strategy. +`sampling.TCollectorProxy` implements `sampling.Manager` by querying +remote `tcollector` service. Then the server converts +thrift response from sampling manager into JSON and responds to clients. + diff --git a/cmd/agent/app/agent.go b/cmd/agent/app/agent.go new file mode 100644 index 00000000000..4403471e4ce --- /dev/null +++ b/cmd/agent/app/agent.go @@ -0,0 +1,83 @@ +// Copyright (c) 2017 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. + +package app + +import ( + "io" + "net" + "net/http" + + "github.com/uber-go/zap" + + "github.com/uber/jaeger/cmd/agent/app/processors" +) + +// Agent is a composition of all services / components +type Agent struct { + processors []processors.Processor + samplingServer *http.Server + discoveryClient interface{} + logger zap.Logger + closer io.Closer +} + +// NewAgent creates the new Agent. +func NewAgent( + processors []processors.Processor, + samplingServer *http.Server, + discoveryClient interface{}, + logger zap.Logger, +) *Agent { + return &Agent{ + processors: processors, + samplingServer: samplingServer, + discoveryClient: discoveryClient, + logger: logger, + } +} + +// Run runs all of agent UDP and HTTP servers in separate go-routines. +// It returns an error when it's immediately apparent on startup, but +// any errors happening after starting the servers are only logged. +func (a *Agent) Run() error { + listener, err := net.Listen("tcp", a.samplingServer.Addr) + if err != nil { + return err + } + a.closer = listener + go func() { + if err := a.samplingServer.Serve(listener); err != nil { + a.logger.Error("sampling server failure", zap.Error(err)) + } + }() + for _, processor := range a.processors { + go processor.Serve() + } + return nil +} + +// Stop forces all agent go routines to exit. +func (a *Agent) Stop() { + for _, processor := range a.processors { + go processor.Stop() + } + a.closer.Close() +} diff --git a/cmd/agent/app/agent_test.go b/cmd/agent/app/agent_test.go new file mode 100644 index 00000000000..7254fe30a02 --- /dev/null +++ b/cmd/agent/app/agent_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2017 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. + +package app + +import ( + "fmt" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber-go/zap" + "github.com/uber/jaeger-lib/metrics" +) + +func TestAgentStartError(t *testing.T) { + cfg := &Builder{} + agent, err := cfg.CreateAgent(metrics.NullFactory, zap.New(zap.NullEncoder())) + require.NoError(t, err) + agent.samplingServer.Addr = "bad-address" + assert.Error(t, agent.Run()) +} + +func TestAgentStartStop(t *testing.T) { + cfg := Builder{ + Processors: []ProcessorConfiguration{ + { + Model: jaegerModel, + Protocol: compactProtocol, + Server: ServerConfiguration{ + HostPort: "127.0.0.1:0", + }, + }, + }, + } + agent, err := cfg.CreateAgent(metrics.NullFactory, zap.New(zap.NullEncoder())) + require.NoError(t, err) + ch := make(chan error, 2) + go func() { + if err := agent.Run(); err != nil { + t.Errorf("error from agent.Run(): %s", err) + ch <- err + } + close(ch) + }() + + url := fmt.Sprintf("http://%s/sampling?service=abc", agent.samplingServer.Addr) + httpClient := &http.Client{ + Timeout: 100 * time.Millisecond, + } + for i := 0; i < 1000; i++ { + _, err := httpClient.Get(url) + if err == nil { + break + } + select { + case err := <-ch: + if err != nil { + t.Fatalf("error from agent: %s", err) + } + break + default: + time.Sleep(time.Millisecond) + } + } + resp, err := http.Get(url) + require.NoError(t, err) + body, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "tcollector error: no peers available\n", string(body)) + agent.Stop() + assert.NoError(t, <-ch) +} diff --git a/cmd/agent/app/config.go b/cmd/agent/app/config.go new file mode 100644 index 00000000000..4b183c14e1d --- /dev/null +++ b/cmd/agent/app/config.go @@ -0,0 +1,299 @@ +// Copyright (c) 2017 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. + +package app + +import ( + "fmt" + "net/http" + + "github.com/apache/thrift/lib/go/thrift" + "github.com/pkg/errors" + "github.com/uber-go/zap" + "github.com/uber/tchannel-go" + tchannelThrift "github.com/uber/tchannel-go/thrift" + + "github.com/uber/jaeger-lib/metrics" + zipkinThrift "github.com/uber/jaeger/thrift-gen/agent" + jaegerThrift "github.com/uber/jaeger/thrift-gen/jaeger" + + "github.com/uber/jaeger/cmd/agent/app/processors" + "github.com/uber/jaeger/cmd/agent/app/reporter" + "github.com/uber/jaeger/cmd/agent/app/sampling" + "github.com/uber/jaeger/cmd/agent/app/servers" + "github.com/uber/jaeger/cmd/agent/app/servers/thriftudp" + "github.com/uber/jaeger/pkg/discovery" + "github.com/uber/jaeger/pkg/discovery/peerlistmgr" +) + +const ( + defaultQueueSize = 1000 + defaultMaxPacketSize = 65000 + defaultServerWorkers = 10 + defaultMinPeers = 3 + + defaultSamplingServerHostPort = "localhost:5778" + + agentServiceName = "jaeger-agent" + collectorServiceName = "tcollector" // for legacy reasons + + jaegerModel model = "jaeger" + zipkinModel = "zipkin" + + compactProtocol protocol = "compact" + binaryProtocol = "binary" +) + +type model string +type protocol string + +var ( + protocolFactoryMap = map[protocol]thrift.TProtocolFactory{ + compactProtocol: thrift.NewTCompactProtocolFactory(), + binaryProtocol: thrift.NewTBinaryProtocolFactoryDefault(), + } +) + +// Builder Struct to hold configurations +type Builder struct { + Processors []ProcessorConfiguration `yaml:"processors"` + SamplingServer SamplingServerConfiguration `yaml:"samplingServer"` + + // CollectorHostPort is the hostPort for Jaeger Collector. + // Set this to communicate with Collector directly, without routing layer. + CollectorHostPort string `yaml:"collectorHostPort"` + + // MinPeers is the min number of servers we want the agent to connect to. + // If zero, defaults to min(3, number of peers returned by service discovery) + DiscoveryMinPeers int `yaml:"minPeers"` + + discoverer discovery.Discoverer + notifier discovery.Notifier + otherReporters []reporter.Reporter +} + +// NewBuilder creates a default builder with three processors. +func NewBuilder() *Builder { + return &Builder{ + Processors: []ProcessorConfiguration{ + { + Workers: defaultServerWorkers, + Model: zipkinModel, + Protocol: compactProtocol, + Server: ServerConfiguration{ + QueueSize: defaultQueueSize, + MaxPacketSize: defaultMaxPacketSize, + HostPort: "127.0.0.1:5775", + }, + }, + { + Workers: defaultServerWorkers, + Model: jaegerModel, + Protocol: compactProtocol, + Server: ServerConfiguration{ + QueueSize: defaultQueueSize, + MaxPacketSize: defaultMaxPacketSize, + HostPort: "127.0.0.1:6831", + }, + }, + { + Workers: defaultServerWorkers, + Model: jaegerModel, + Protocol: binaryProtocol, + Server: ServerConfiguration{ + QueueSize: defaultQueueSize, + MaxPacketSize: defaultMaxPacketSize, + HostPort: "127.0.0.1:6832", + }, + }, + }, + SamplingServer: SamplingServerConfiguration{ + HostPort: "127.0.0.1:5778", + }, + } +} + +// ProcessorConfiguration holds config for a processor that receives spans from Server +type ProcessorConfiguration struct { + Workers int `yaml:"workers"` + Model model `yaml:"model"` + Protocol protocol `yaml:"protocol"` + Server ServerConfiguration `yaml:"server"` +} + +// ServerConfiguration holds config for a server that receives spans from the network +type ServerConfiguration struct { + QueueSize int `yaml:"queueSize"` + MaxPacketSize int `yaml:"maxPacketSize"` + HostPort string `yaml:"hostPort" validate:"nonzero"` +} + +// SamplingServerConfiguration holds config for a server providing sampling strategies to clients +type SamplingServerConfiguration struct { + HostPort string `yaml:"hostPort" validate:"nonzero"` +} + +// WithReporter adds auxilary reporters +func (b *Builder) WithReporter(r reporter.Reporter) *Builder { + b.otherReporters = append(b.otherReporters, r) + return b +} + +// WithDiscoverer sets service discovery +func (b *Builder) WithDiscoverer(d discovery.Discoverer) *Builder { + b.discoverer = d + return b +} + +// WithDiscoveryNotifier sets service discovery notifier +func (b *Builder) WithDiscoveryNotifier(n discovery.Notifier) *Builder { + b.notifier = n + return b +} + +func (b *Builder) enableDiscovery(channel *tchannel.Channel, logger zap.Logger) (interface{}, error) { + if b.discoverer == nil && b.notifier == nil { + return nil, nil + } + if b.discoverer == nil || b.notifier == nil { + return nil, errors.New("both discovery.Discoverer and discovery.Notifier must be specified") + } + + logger.Info("Enabling service discovery", zap.String("service", collectorServiceName)) + + subCh := channel.GetSubChannel(collectorServiceName, tchannel.Isolated) + peers := subCh.Peers() + return peerlistmgr.New(peers, b.discoverer, b.notifier, + peerlistmgr.Options.MinPeers(defaultInt(b.DiscoveryMinPeers, defaultMinPeers)), + peerlistmgr.Options.Logger(logger)) +} + +// CreateAgent creates the Agent +func (b *Builder) CreateAgent(mFactory metrics.Factory, logger zap.Logger) (*Agent, error) { + // ignore errors since it only happens on empty service name + channel, _ := tchannel.NewChannel(agentServiceName, nil) + + discoveryMgr, err := b.enableDiscovery(channel, logger) + if err != nil { + return nil, errors.Wrap(err, "cannot enable service discovery") + } + var clientOpts *tchannelThrift.ClientOptions + if discoveryMgr == nil && b.CollectorHostPort != "" { + clientOpts = &tchannelThrift.ClientOptions{HostPort: b.CollectorHostPort} + } + rep := reporter.NewTCollectorReporter(channel, mFactory, logger, clientOpts) + if b.otherReporters != nil { + reps := append([]reporter.Reporter{}, b.otherReporters...) + reps = append(reps, rep) + rep = reporter.NewMultiReporter(reps...) + } + processors, err := b.GetProcessors(rep, mFactory) + if err != nil { + return nil, err + } + samplingServer := b.SamplingServer.GetSamplingServer(channel, mFactory, clientOpts) + return NewAgent(processors, samplingServer, discoveryMgr, logger), nil +} + +// GetProcessors creates Processors with attached Reporter +func (b *Builder) GetProcessors(rep reporter.Reporter, mFactory metrics.Factory) ([]processors.Processor, error) { + retMe := make([]processors.Processor, len(b.Processors)) + for idx, cfg := range b.Processors { + protoFactory, ok := protocolFactoryMap[cfg.Protocol] + if !ok { + return nil, fmt.Errorf("cannot find protocol factory for protocol %v", cfg.Protocol) + } + var handler processors.AgentProcessor + switch cfg.Model { + case jaegerModel: + handler = jaegerThrift.NewAgentProcessor(rep) + case zipkinModel: + handler = zipkinThrift.NewAgentProcessor(rep) + default: + return nil, fmt.Errorf("cannot find agent processor for data model %v", cfg.Model) + } + metrics := mFactory.Namespace("", map[string]string{ + "protocol": string(cfg.Protocol), + "model": string(cfg.Model), + }) + processor, err := cfg.GetThriftProcessor(metrics, protoFactory, handler) + if err != nil { + return nil, err + } + retMe[idx] = processor + } + return retMe, nil +} + +// GetSamplingServer creates an HTTP server that provides sampling strategies to client libraries. +func (c SamplingServerConfiguration) GetSamplingServer(channel *tchannel.Channel, mFactory metrics.Factory, clientOpts *tchannelThrift.ClientOptions) *http.Server { + samplingMgr := sampling.NewTCollectorSamplingManagerProxy(channel, mFactory, clientOpts) + if c.HostPort == "" { + c.HostPort = defaultSamplingServerHostPort + } + return sampling.NewSamplingServer(c.HostPort, samplingMgr, mFactory) +} + +// GetThriftProcessor gets a TBufferedServer backed Processor using the collector configuration +func (c *ProcessorConfiguration) GetThriftProcessor( + mFactory metrics.Factory, + factory thrift.TProtocolFactory, + handler processors.AgentProcessor, +) (processors.Processor, error) { + c.applyDefaults() + + server, err := c.Server.getUDPServer(mFactory) + if err != nil { + return nil, err + } + + return processors.NewThriftProcessor(server, c.Workers, mFactory, factory, handler) +} + +func (c *ProcessorConfiguration) applyDefaults() { + c.Workers = defaultInt(c.Workers, defaultServerWorkers) +} + +func (c *ServerConfiguration) applyDefaults() { + c.QueueSize = defaultInt(c.QueueSize, defaultQueueSize) + c.MaxPacketSize = defaultInt(c.MaxPacketSize, defaultMaxPacketSize) +} + +// getUDPServer gets a TBufferedServer backed server using the server configuration +func (c *ServerConfiguration) getUDPServer(mFactory metrics.Factory) (servers.Server, error) { + c.applyDefaults() + + if c.HostPort == "" { + return nil, fmt.Errorf("no host:port provided for udp server: %+v", *c) + } + transport, err := thriftudp.NewTUDPServerTransport(c.HostPort) + if err != nil { + return nil, err + } + + return servers.NewTBufferedServer(transport, c.QueueSize, c.MaxPacketSize, mFactory) +} + +func defaultInt(value int, defaultVal int) int { + if value == 0 { + value = defaultVal + } + return value +} diff --git a/cmd/agent/app/config_test.go b/cmd/agent/app/config_test.go new file mode 100644 index 00000000000..01260eebfb1 --- /dev/null +++ b/cmd/agent/app/config_test.go @@ -0,0 +1,159 @@ +// Copyright (c) 2017 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. + +package app + +import ( + "io/ioutil" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber-go/zap" + "github.com/uber/jaeger-lib/metrics" + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" + "gopkg.in/yaml.v2" + + "github.com/uber/jaeger/pkg/discovery" +) + +func TestConfigFile(t *testing.T) { + cfg := Builder{} + data, err := ioutil.ReadFile("testdata/test_config.yaml") + require.NoError(t, err) + err = yaml.Unmarshal(data, &cfg) + require.NoError(t, err) + assert.Len(t, cfg.Processors, 3) + for i := range cfg.Processors { + cfg.Processors[i].applyDefaults() + cfg.Processors[i].Server.applyDefaults() + } + assert.Equal(t, ProcessorConfiguration{ + Model: zipkinModel, + Protocol: compactProtocol, + Workers: 10, + Server: ServerConfiguration{ + QueueSize: 1000, + MaxPacketSize: 65000, + HostPort: "1.1.1.1:5775", + }, + }, cfg.Processors[0]) + assert.Equal(t, ProcessorConfiguration{ + Model: jaegerModel, + Protocol: compactProtocol, + Workers: 10, + Server: ServerConfiguration{ + QueueSize: 1000, + MaxPacketSize: 65000, + HostPort: "2.2.2.2:6831", + }, + }, cfg.Processors[1]) + assert.Equal(t, ProcessorConfiguration{ + Model: jaegerModel, + Protocol: binaryProtocol, + Workers: 20, + Server: ServerConfiguration{ + QueueSize: 2000, + MaxPacketSize: 65001, + HostPort: "3.3.3.3:6832", + }, + }, cfg.Processors[2]) + assert.Equal(t, "4.4.4.4:5778", cfg.SamplingServer.HostPort) +} + +func TestConfigWithDiscovery(t *testing.T) { + cfg := &Builder{} + discoverer := discovery.FixedDiscoverer([]string{"1.1.1.1:80"}) + cfg.WithDiscoverer(discoverer) + _, err := cfg.CreateAgent(metrics.NullFactory, zap.New(zap.NullEncoder())) + assert.EqualError(t, err, "cannot enable service discovery: both discovery.Discoverer and discovery.Notifier must be specified") + + cfg = &Builder{} + notifier := &discovery.Dispatcher{} + cfg.WithDiscoverer(discoverer).WithDiscoveryNotifier(notifier) + agent, err := cfg.CreateAgent(metrics.NullFactory, zap.New(zap.NullEncoder())) + assert.NoError(t, err) + assert.NotNil(t, agent) +} + +func TestConfigWithSingleCollector(t *testing.T) { + cfg := &Builder{ + CollectorHostPort: "127.0.0.1:9876", + } + agent, err := cfg.CreateAgent(metrics.NullFactory, zap.New(zap.NullEncoder())) + assert.NoError(t, err) + assert.NotNil(t, agent) +} + +type fakeReporter struct{} + +func (fr fakeReporter) EmitZipkinBatch(spans []*zipkincore.Span) (err error) { + return nil +} + +func (fr fakeReporter) EmitBatch(batch *jaeger.Batch) (err error) { + return nil +} + +func TestConfigWithExtraReporter(t *testing.T) { + cfg := &Builder{} + cfg.WithReporter(fakeReporter{}) + agent, err := cfg.CreateAgent(metrics.NullFactory, zap.New(zap.NullEncoder())) + assert.NoError(t, err) + assert.NotNil(t, agent) +} + +func TestConfigWithProcessorErrors(t *testing.T) { + testCases := []struct { + model model + protocol protocol + hostPort string + err string + errContains string + }{ + {protocol: protocol("bad"), err: "cannot find protocol factory for protocol bad"}, + {protocol: compactProtocol, model: model("bad"), err: "cannot find agent processor for data model bad"}, + {protocol: compactProtocol, model: jaegerModel, err: "no host:port provided for udp server: {QueueSize:1000 MaxPacketSize:65000 HostPort:}"}, + {protocol: compactProtocol, model: zipkinModel, hostPort: "bad-host-port", errContains: "bad-host-port"}, + } + for _, tc := range testCases { + testCase := tc // capture loop var + cfg := &Builder{ + Processors: []ProcessorConfiguration{ + { + Model: testCase.model, + Protocol: testCase.protocol, + Server: ServerConfiguration{ + HostPort: testCase.hostPort, + }, + }, + }, + } + _, err := cfg.CreateAgent(metrics.NullFactory, zap.New(zap.NullEncoder())) + assert.Error(t, err) + if testCase.err != "" { + assert.EqualError(t, err, testCase.err) + } else if testCase.errContains != "" { + assert.True(t, strings.Contains(err.Error(), testCase.errContains), "error must contain %s", testCase.errContains) + } + } +} diff --git a/cmd/agent/app/customtransports/buffered_read_transport.go b/cmd/agent/app/customtransports/buffered_read_transport.go new file mode 100644 index 00000000000..95b6d8f084e --- /dev/null +++ b/cmd/agent/app/customtransports/buffered_read_transport.go @@ -0,0 +1,79 @@ +// Copyright (c) 2017 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. + +package customtransport + +import ( + "bytes" + + "github.com/apache/thrift/lib/go/thrift" +) + +// TBufferedReadTransport is a thrift.TTransport that reads from a buffer +type TBufferedReadTransport struct { + readBuf *bytes.Buffer +} + +// NewTBufferedReadTransport creates a buffer backed TTransport +func NewTBufferedReadTransport(readBuf *bytes.Buffer) (*TBufferedReadTransport, error) { + return &TBufferedReadTransport{readBuf: readBuf}, nil +} + +// IsOpen does nothing as transport is not maintaining the connection +// Required to maintain thrift.TTransport interface +func (p *TBufferedReadTransport) IsOpen() bool { + return true +} + +// Open does nothing as transport is not maintaining the connection +// Required to maintain thrift.TTransport interface +func (p *TBufferedReadTransport) Open() error { + return nil +} + +// Close does nothing as transport is not maintaining the connection +// Required to maintain thrift.TTransport interface +func (p *TBufferedReadTransport) Close() error { + return nil +} + +// Read reads bytes from the local buffer and puts them in the specified buf +func (p *TBufferedReadTransport) Read(buf []byte) (int, error) { + in, err := p.readBuf.Read(buf) + return in, thrift.NewTTransportExceptionFromError(err) +} + +// RemainingBytes returns the number of bytes left to be read from the readBuf +func (p *TBufferedReadTransport) RemainingBytes() uint64 { + return uint64(p.readBuf.Len()) +} + +// Write writes bytes into the read buffer +// Required to maintain thrift.TTransport interface +func (p *TBufferedReadTransport) Write(buf []byte) (int, error) { + p.readBuf = bytes.NewBuffer(buf) + return len(buf), nil +} + +// Flush does nothing as udp server does not write responses back +// Required to maintain thrift.TTransport interface +func (p *TBufferedReadTransport) Flush() error { + return nil +} diff --git a/cmd/agent/app/customtransports/buffered_read_transport_test.go b/cmd/agent/app/customtransports/buffered_read_transport_test.go new file mode 100644 index 00000000000..411f83c08bd --- /dev/null +++ b/cmd/agent/app/customtransports/buffered_read_transport_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2017 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. + +package customtransport + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestTBufferedReadTransport tests the TBufferedReadTransport +func TestTBufferedReadTransport(t *testing.T) { + buffer := bytes.NewBuffer([]byte("testString")) + trans, err := NewTBufferedReadTransport(buffer) + require.NotNil(t, trans) + require.Nil(t, err) + require.Equal(t, uint64(10), trans.RemainingBytes()) + + firstRead := make([]byte, 4) + n, err := trans.Read(firstRead) + require.Nil(t, err) + require.Equal(t, 4, n) + require.Equal(t, []byte("test"), firstRead) + require.Equal(t, uint64(6), trans.RemainingBytes()) + + secondRead := make([]byte, 7) + n, err = trans.Read(secondRead) + require.Equal(t, 6, n) + require.Equal(t, []byte("String"), secondRead[0:6]) + require.Equal(t, uint64(0), trans.RemainingBytes()) +} + +// TestTBufferedReadTransportEmptyFunctions tests the empty functions in TBufferedReadTransport +func TestTBufferedReadTransportEmptyFunctions(t *testing.T) { + byteArr := make([]byte, 1) + trans, err := NewTBufferedReadTransport(bytes.NewBuffer(byteArr)) + require.NotNil(t, trans) + require.Nil(t, err) + + err = trans.Open() + require.Nil(t, err) + + err = trans.Close() + require.Nil(t, err) + + err = trans.Flush() + require.Nil(t, err) + + n, err := trans.Write(byteArr) + require.Equal(t, 1, n) + require.Nil(t, err) + + isOpen := trans.IsOpen() + require.True(t, isOpen) +} diff --git a/cmd/agent/app/flags.go b/cmd/agent/app/flags.go new file mode 100644 index 00000000000..0c400d92c31 --- /dev/null +++ b/cmd/agent/app/flags.go @@ -0,0 +1,66 @@ +// Copyright (c) 2017 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. + +package app + +import "flag" + +// Bind binds the agent builder to command line options +func (b *Builder) Bind(flags *flag.FlagSet) { + for i := range b.Processors { + p := &b.Processors[i] + name := "processor." + string(p.Model) + "-" + string(p.Protocol) + "." + flags.IntVar( + &p.Workers, + name+"workers", + p.Workers, + "how many workers the processor should run") + flags.IntVar( + &p.Server.QueueSize, + name+"server-queue-size", + p.Server.QueueSize, + "length of the queue for the UDP server") + flags.IntVar( + &p.Server.MaxPacketSize, + name+"server-max-packet-size", + p.Server.MaxPacketSize, + "max packet size for the UDP server") + flags.StringVar( + &p.Server.HostPort, + name+"server-host-port", + p.Server.HostPort, + "host:port for the UDP server") + } + flags.StringVar( + &b.CollectorHostPort, + "collector.host-port", + "", + "host:port of a single collector to connect to directly (e.g. when not using service discovery)") + flags.StringVar( + &b.SamplingServer.HostPort, + "http-server.host-port", + b.SamplingServer.HostPort, + "host:port of the http server (e.g. for /sampling point)") + flags.IntVar( + &b.DiscoveryMinPeers, + "discovery.min-peers", + 3, + "if using service discovery, the min number of connections to maintain to the backend") +} diff --git a/cmd/agent/app/flags_test.go b/cmd/agent/app/flags_test.go new file mode 100644 index 00000000000..96e66b27b35 --- /dev/null +++ b/cmd/agent/app/flags_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2017 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. + +package app + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBingFlags(t *testing.T) { + cfg := NewBuilder() + flags := flag.NewFlagSet("test", flag.ExitOnError) + cfg.Bind(flags) + flags.Parse([]string{ + "-collector.host-port=1.2.3.4:555", + "-discovery.min-peers=42", + "-http-server.host-port=:8080", + "-processor.jaeger-binary.server-host-port=:1111", + "-processor.jaeger-binary.server-max-packet-size=4242", + "-processor.jaeger-binary.server-queue-size=42", + "-processor.jaeger-binary.workers=42", + }) + assert.Equal(t, 3, len(cfg.Processors)) + assert.Equal(t, "1.2.3.4:555", cfg.CollectorHostPort) + assert.Equal(t, 42, cfg.DiscoveryMinPeers) + assert.Equal(t, ":8080", cfg.SamplingServer.HostPort) + assert.Equal(t, ":1111", cfg.Processors[2].Server.HostPort) + assert.Equal(t, 4242, cfg.Processors[2].Server.MaxPacketSize) + assert.Equal(t, 42, cfg.Processors[2].Server.QueueSize) + assert.Equal(t, 42, cfg.Processors[2].Workers) +} diff --git a/cmd/agent/app/processors/processor.go b/cmd/agent/app/processors/processor.go new file mode 100644 index 00000000000..6a0b32ef5b6 --- /dev/null +++ b/cmd/agent/app/processors/processor.go @@ -0,0 +1,27 @@ +// Copyright (c) 2017 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. + +package processors + +// Processor processes metrics in multiple formats +type Processor interface { + Serve() + Stop() +} diff --git a/cmd/agent/app/processors/thrift_processor.go b/cmd/agent/app/processors/thrift_processor.go new file mode 100644 index 00000000000..2fb5855f322 --- /dev/null +++ b/cmd/agent/app/processors/thrift_processor.go @@ -0,0 +1,123 @@ +// Copyright (c) 2017 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. + +package processors + +import ( + "fmt" + "sync" + + "github.com/apache/thrift/lib/go/thrift" + "github.com/uber/jaeger-lib/metrics" + + "github.com/uber/jaeger/cmd/agent/app/customtransports" + "github.com/uber/jaeger/cmd/agent/app/servers" +) + +// ThriftProcessor is a server that processes spans using a TBuffered Server +type ThriftProcessor struct { + server servers.Server + handler AgentProcessor + protocolPool *sync.Pool + numProcessors int + processing sync.WaitGroup + metrics struct { + // Amount of time taken for processor to close + ProcessorCloseTimer metrics.Timer `metric:"thrift.udp.t-processor.close-time"` + + // Number of failed buffer process operations + HandlerProcessError metrics.Counter `metric:"thrift.udp.t-processor.handler-errors"` + } +} + +// AgentProcessor handler used by the processor to process thrift and call the reporter with the deserialized struct +type AgentProcessor interface { + Process(iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) +} + +// NewThriftProcessor creates a TBufferedServer backed ThriftProcessor +func NewThriftProcessor( + server servers.Server, + numProcessors int, + mFactory metrics.Factory, + factory thrift.TProtocolFactory, + handler AgentProcessor, +) (*ThriftProcessor, error) { + if numProcessors <= 0 { + return nil, fmt.Errorf( + "Number of processors must be greater than 0, called with %d", numProcessors) + } + var protocolPool = &sync.Pool{ + New: func() interface{} { + trans := &customtransport.TBufferedReadTransport{} + return factory.GetProtocol(trans) + }, + } + + res := &ThriftProcessor{ + server: server, + handler: handler, + protocolPool: protocolPool, + numProcessors: numProcessors, + } + metrics.Init(&res.metrics, mFactory, nil) + return res, nil +} + +// Serve initiates the readers and starts serving traffic +func (s *ThriftProcessor) Serve() { + s.processing.Add(s.numProcessors) + for i := 0; i < s.numProcessors; i++ { + go s.processBuffer() + } + + s.server.Serve() +} + +// IsServing indicates whether the server is currently serving traffic +func (s *ThriftProcessor) IsServing() bool { + return s.server.IsServing() +} + +// Stop stops the serving of traffic and waits until the queue is +// emptied by the readers +func (s *ThriftProcessor) Stop() { + stopwatch := metrics.StartStopwatch(s.metrics.ProcessorCloseTimer) + s.server.Stop() + s.processing.Wait() + stopwatch.Stop() +} + +// processBuffer reads data off the channel and puts it into a custom transport for +// the processor to process +func (s *ThriftProcessor) processBuffer() { + for readBuf := range s.server.DataChan() { + protocol := s.protocolPool.Get().(thrift.TProtocol) + protocol.Transport().Write(readBuf.GetBytes()) + s.server.DataRecd(readBuf) // acknowledge receipt and release the buffer + + if ok, _ := s.handler.Process(protocol, protocol); !ok { + // TODO log the error + s.metrics.HandlerProcessError.Inc(1) + } + s.protocolPool.Put(protocol) + } + s.processing.Done() +} diff --git a/cmd/agent/app/processors/thrift_processor_test.go b/cmd/agent/app/processors/thrift_processor_test.go new file mode 100644 index 00000000000..2270ad6b617 --- /dev/null +++ b/cmd/agent/app/processors/thrift_processor_test.go @@ -0,0 +1,237 @@ +// Copyright (c) 2017 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. + +package processors + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/apache/thrift/lib/go/thrift" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber-go/zap" + + "github.com/uber/jaeger-lib/metrics" + mTestutils "github.com/uber/jaeger-lib/metrics/testutils" + "github.com/uber/jaeger/thrift-gen/agent" + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" + + "github.com/uber/jaeger/cmd/agent/app/reporter" + "github.com/uber/jaeger/cmd/agent/app/servers" + "github.com/uber/jaeger/cmd/agent/app/servers/thriftudp" + "github.com/uber/jaeger/cmd/agent/app/testutils" +) + +// TODO make these tests faster, they take almost 4 seconds + +var ( + compactFactory = thrift.NewTCompactProtocolFactory() + binaryFactory = thrift.NewTBinaryProtocolFactoryDefault() + + testSpanName = "span1" + + batch = &jaeger.Batch{ + Process: jaeger.NewProcess(), + Spans: []*jaeger.Span{{OperationName: testSpanName}}, + } +) + +func createProcessor(t *testing.T, mFactory metrics.Factory, tFactory thrift.TProtocolFactory, handler AgentProcessor) (string, Processor) { + transport, err := thriftudp.NewTUDPServerTransport("127.0.0.1:0") + require.NoError(t, err) + + queueSize := 10 + maxPacketSize := 65000 + server, err := servers.NewTBufferedServer(transport, queueSize, maxPacketSize, mFactory) + require.NoError(t, err) + + numProcessors := 1 + processor, err := NewThriftProcessor(server, numProcessors, mFactory, tFactory, handler) + require.NoError(t, err) + + go processor.Serve() + for i := 0; i < 1000; i++ { + if processor.IsServing() { + break + } + time.Sleep(10 * time.Microsecond) + } + require.True(t, processor.IsServing(), "processor must be serving") + + return transport.Addr().String(), processor +} + +func initCollectorAndReporter(t *testing.T) (*metrics.LocalFactory, *testutils.MockTCollector, reporter.Reporter) { + metricsFactory, collector := testutils.InitMockCollector(t) + + reporter := reporter.NewTCollectorReporter(collector.Channel, metricsFactory, zap.New(zap.NullEncoder()), nil) + + return metricsFactory, collector, reporter +} + +func TestNewThriftProcessor_ZeroCount(t *testing.T) { + _, err := NewThriftProcessor(nil, 0, nil, nil, nil) + assert.EqualError(t, err, "Number of processors must be greater than 0, called with 0") +} + +func TestProcessorWithCompactZipkin(t *testing.T) { + metricsFactory, collector, reporter := initCollectorAndReporter(t) + defer collector.Close() + + hostPort, processor := createProcessor(t, metricsFactory, compactFactory, agent.NewAgentProcessor(reporter)) + defer processor.Stop() + + client, clientCloser, err := testutils.NewZipkinThriftUDPClient(hostPort) + require.NoError(t, err) + defer clientCloser.Close() + + span := zipkincore.NewSpan() + span.Name = testSpanName + + err = client.EmitZipkinBatch([]*zipkincore.Span{span}) + require.NoError(t, err) + + assertZipkinProcessorCorrectness(t, collector, metricsFactory) +} + +type failingHandler struct { + err error +} + +func (h failingHandler) Process(iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + return false, thrift.NewTApplicationException(0, h.err.Error()) +} + +func TestProcessor_HandlerError(t *testing.T) { + metricsFactory := metrics.NewLocalFactory(0) + + handler := failingHandler{err: errors.New("doh")} + + hostPort, processor := createProcessor(t, metricsFactory, compactFactory, handler) + defer processor.Stop() + + client, clientCloser, err := testutils.NewZipkinThriftUDPClient(hostPort) + require.NoError(t, err) + defer clientCloser.Close() + + err = client.EmitZipkinBatch([]*zipkincore.Span{{Name: testSpanName}}) + require.NoError(t, err) + + for i := 0; i < 10; i++ { + c, _ := metricsFactory.Snapshot() + if _, ok := c["thrift.udp.t-processor.handler-errors"]; ok { + break + } + time.Sleep(time.Millisecond) + } + + mTestutils.AssertCounterMetrics(t, metricsFactory, + mTestutils.ExpectedMetric{Name: "thrift.udp.t-processor.handler-errors", Value: 1}, + mTestutils.ExpectedMetric{Name: "thrift.udp.server.packets.processed", Value: 1}, + ) +} + +func TestJaegerProcessor(t *testing.T) { + tests := []struct { + factory thrift.TProtocolFactory + }{ + {compactFactory}, + {binaryFactory}, + } + + for _, test := range tests { + metricsFactory, collector, reporter := initCollectorAndReporter(t) + + hostPort, processor := createProcessor(t, metricsFactory, test.factory, jaeger.NewAgentProcessor(reporter)) + + client, clientCloser, err := testutils.NewJaegerThriftUDPClient(hostPort, test.factory) + require.NoError(t, err) + + err = client.EmitBatch(batch) + require.NoError(t, err) + + assertJaegerProcessorCorrectness(t, collector, metricsFactory) + + processor.Stop() + clientCloser.Close() + collector.Close() + } +} + +func assertJaegerProcessorCorrectness(t *testing.T, collector *testutils.MockTCollector, metricsFactory *metrics.LocalFactory) { + sizeF := func() int { + return len(collector.GetJaegerBatches()) + } + nameF := func() string { + return collector.GetJaegerBatches()[0].Spans[0].OperationName + } + assertProcessorCorrectness(t, metricsFactory, sizeF, nameF, "jaeger") +} + +func assertZipkinProcessorCorrectness(t *testing.T, collector *testutils.MockTCollector, metricsFactory *metrics.LocalFactory) { + sizeF := func() int { + return len(collector.GetZipkinSpans()) + } + nameF := func() string { + return collector.GetZipkinSpans()[0].Name + } + assertProcessorCorrectness(t, metricsFactory, sizeF, nameF, "zipkin") +} + +func assertProcessorCorrectness( + t *testing.T, + metricsFactory *metrics.LocalFactory, + sizeF func() int, + nameF func() string, + counterType string, +) { + // wait for server to receive + for i := 0; i < 1000; i++ { + if sizeF() == 1 { + break + } + time.Sleep(1 * time.Millisecond) + } + + require.Equal(t, 1, sizeF()) + assert.Equal(t, testSpanName, nameF()) + + // wait for reporter to emit metrics + for i := 0; i < 1000; i++ { + c, _ := metricsFactory.Snapshot() + if _, ok := c["tc-reporter.spans.submitted"]; ok { + break + } + time.Sleep(1 * time.Millisecond) + } + + // agentReporter must emit metrics + batchesSubmittedCounter := fmt.Sprintf("tc-reporter.%s.batches.submitted", counterType) + spansSubmittedCounter := fmt.Sprintf("tc-reporter.%s.spans.submitted", counterType) + mTestutils.AssertCounterMetrics(t, metricsFactory, []mTestutils.ExpectedMetric{ + {Name: batchesSubmittedCounter, Value: 1}, + {Name: spansSubmittedCounter, Value: 1}, + {Name: "thrift.udp.server.packets.processed", Value: 1}, + }...) +} diff --git a/cmd/agent/app/reporter/reporter.go b/cmd/agent/app/reporter/reporter.go new file mode 100644 index 00000000000..81e23557f35 --- /dev/null +++ b/cmd/agent/app/reporter/reporter.go @@ -0,0 +1,67 @@ +// Copyright (c) 2017 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. + +package reporter + +import ( + "github.com/uber/jaeger/pkg/multierror" + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" +) + +// Reporter handles spans received by Processor and forwards them to central +// collectors. +type Reporter interface { + EmitZipkinBatch(spans []*zipkincore.Span) (err error) + EmitBatch(batch *jaeger.Batch) (err error) +} + +// MultiReporter provides serial span emission to one or more reporters. If +// more than one expensive reporter are needed, one or more of them should be +// wrapped and hidden behind a channel. +type MultiReporter []Reporter + +// NewMultiReporter creates a MultiReporter from the variadic list of passed +// Reporters. +func NewMultiReporter(reps ...Reporter) MultiReporter { + return reps +} + +// EmitZipkinBatch calls each EmitZipkinBatch, returning the first error. +func (mr MultiReporter) EmitZipkinBatch(spans []*zipkincore.Span) error { + var errors []error + for _, rep := range mr { + if err := rep.EmitZipkinBatch(spans); err != nil { + errors = append(errors, err) + } + } + return multierror.Wrap(errors) +} + +// EmitBatch calls each EmitBatch, returning the first error. +func (mr MultiReporter) EmitBatch(batch *jaeger.Batch) error { + var errors []error + for _, rep := range mr { + if err := rep.EmitBatch(batch); err != nil { + errors = append(errors, err) + } + } + return multierror.Wrap(errors) +} diff --git a/cmd/agent/app/reporter/reporter_test.go b/cmd/agent/app/reporter/reporter_test.go new file mode 100644 index 00000000000..de21fe3cfe6 --- /dev/null +++ b/cmd/agent/app/reporter/reporter_test.go @@ -0,0 +1,81 @@ +// Copyright (c) 2017 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. + +package reporter + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" + + "github.com/uber/jaeger/cmd/agent/app/testutils" +) + +func TestMultiReporter(t *testing.T) { + r1, r2 := testutils.NewInMemoryReporter(), testutils.NewInMemoryReporter() + r := NewMultiReporter(r1, r2) + e1 := r.EmitZipkinBatch([]*zipkincore.Span{ + {}, + }) + e2 := r.EmitBatch(&jaeger.Batch{ + Spans: []*jaeger.Span{ + {}, + }, + }) + assert.NoError(t, e1) + assert.NoError(t, e2) + assert.Len(t, r1.ZipkinSpans(), 1) + assert.Len(t, r1.Spans(), 1) + assert.Len(t, r2.ZipkinSpans(), 1) + assert.Len(t, r2.Spans(), 1) +} + +func TestMultiReporterErrors(t *testing.T) { + errMsg := "doh!" + err := errors.New(errMsg) + r1, r2 := alwaysFailReporter{err: err}, alwaysFailReporter{err: err} + r := NewMultiReporter(r1, r2) + e1 := r.EmitZipkinBatch([]*zipkincore.Span{ + {}, + }) + e2 := r.EmitBatch(&jaeger.Batch{ + Spans: []*jaeger.Span{ + {}, + }, + }) + assert.EqualError(t, e1, fmt.Sprintf("[%s, %s]", errMsg, errMsg)) + assert.EqualError(t, e2, fmt.Sprintf("[%s, %s]", errMsg, errMsg)) +} + +type alwaysFailReporter struct { + err error +} + +func (r alwaysFailReporter) EmitZipkinBatch(spans []*zipkincore.Span) error { + return r.err +} + +func (r alwaysFailReporter) EmitBatch(batch *jaeger.Batch) error { + return r.err +} diff --git a/cmd/agent/app/reporter/tcollector_reporter.go b/cmd/agent/app/reporter/tcollector_reporter.go new file mode 100644 index 00000000000..cf775b4ecf9 --- /dev/null +++ b/cmd/agent/app/reporter/tcollector_reporter.go @@ -0,0 +1,125 @@ +// Copyright (c) 2017 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. + +package reporter + +import ( + "time" + + "github.com/uber-go/zap" + "github.com/uber/tchannel-go" + "github.com/uber/tchannel-go/thrift" + + "github.com/uber/jaeger-lib/metrics" + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" +) + +const ( + jaegerBatches = "jaeger" + zipkinBatches = "zipkin" +) + +type batchMetrics struct { + // Number of successful batch submissions to collector + BatchesSubmitted metrics.Counter `metric:"batches.submitted"` + + // Number of failed batch submissions to collector + BatchesFailures metrics.Counter `metric:"batches.failures"` + + // Number of spans in a batch submitted to collector + BatchSize metrics.Gauge `metric:"batch_size"` + + // Number of successful span submissions to collector + SpansSubmitted metrics.Counter `metric:"spans.submitted"` + + // Number of failed span submissions to collector + SpansFailures metrics.Counter `metric:"spans.failures"` +} + +// tcollectorReporter forwards received spans to central collector tier +type tcollectorReporter struct { + zClient zipkincore.TChanZipkinCollector + jClient jaeger.TChanCollector + batchesMetrics map[string]batchMetrics + logger zap.Logger +} + +// NewTCollectorReporter creates new tcollectorReporter +func NewTCollectorReporter(channel *tchannel.Channel, mFactory metrics.Factory, zlogger zap.Logger, clientOpts *thrift.ClientOptions) Reporter { + thriftClient := thrift.NewClient(channel, "tcollector", clientOpts) + zClient := zipkincore.NewTChanZipkinCollectorClient(thriftClient) + jClient := jaeger.NewTChanCollectorClient(thriftClient) + batchesMetrics := map[string]batchMetrics{} + tcReporterNS := mFactory.Namespace("tc-reporter", nil) + for _, s := range []string{zipkinBatches, jaegerBatches} { + nsByType := tcReporterNS.Namespace(s, nil) + bm := batchMetrics{} + metrics.Init(&bm, nsByType, nil) + batchesMetrics[s] = bm + } + rep := &tcollectorReporter{zClient: zClient, jClient: jClient, logger: zlogger, batchesMetrics: batchesMetrics} + + return rep +} + +// EmitZipkinBatch implements EmitZipkinBatch() of Reporter +func (r *tcollectorReporter) EmitZipkinBatch(spans []*zipkincore.Span) error { + submissionFunc := func(ctx thrift.Context) error { + _, err := r.zClient.SubmitZipkinBatch(ctx, spans) + return err + } + return r.submitAndReport( + submissionFunc, + "Could not submit zipkin batch", + int64(len(spans)), + r.batchesMetrics[zipkinBatches], + ) +} + +// EmitBatch implements EmitBatch() of Reporter +func (r *tcollectorReporter) EmitBatch(batch *jaeger.Batch) error { + submissionFunc := func(ctx thrift.Context) error { + _, err := r.jClient.SubmitBatches(ctx, []*jaeger.Batch{batch}) + return err + } + return r.submitAndReport( + submissionFunc, + "Could not submit jaeger batch", + int64(len(batch.Spans)), + r.batchesMetrics[jaegerBatches], + ) +} + +func (r *tcollectorReporter) submitAndReport(submissionFunc func(ctx thrift.Context) error, errMsg string, size int64, batchMetrics batchMetrics) error { + ctx, cancel := tchannel.NewContextBuilder(time.Second).DisableTracing().Build() + defer cancel() + + if err := submissionFunc(ctx); err != nil { + batchMetrics.BatchesFailures.Inc(1) + batchMetrics.SpansFailures.Inc(size) + r.logger.Error(errMsg, zap.Error(err)) + return err + } + batchMetrics.BatchSize.Update(size) + batchMetrics.BatchesSubmitted.Inc(1) + batchMetrics.SpansSubmitted.Inc(size) + return nil +} diff --git a/cmd/agent/app/reporter/tcollector_reporter_test.go b/cmd/agent/app/reporter/tcollector_reporter_test.go new file mode 100644 index 00000000000..03818dfec4c --- /dev/null +++ b/cmd/agent/app/reporter/tcollector_reporter_test.go @@ -0,0 +1,128 @@ +// Copyright (c) 2017 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. + +package reporter + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber-go/zap" + "github.com/uber/jaeger-lib/metrics" + mTestutils "github.com/uber/jaeger-lib/metrics/testutils" + + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" + + "github.com/uber/jaeger/cmd/agent/app/testutils" +) + +func initRequirements(t *testing.T) (*metrics.LocalFactory, *testutils.MockTCollector, Reporter) { + metricsFactory, collector := testutils.InitMockCollector(t) + + reporter := NewTCollectorReporter(collector.Channel, metricsFactory, zap.New(zap.NullEncoder()), nil) + + return metricsFactory, collector, reporter +} + +func TestZipkinTCollectorReporterSuccess(t *testing.T) { + metricsFactory, collector, reporter := initRequirements(t) + defer collector.Close() + + require.NoError(t, submitTestZipkinBatch(reporter)) + + time.Sleep(100 * time.Millisecond) // wait for server to receive + + require.Equal(t, 1, len(collector.GetZipkinSpans())) + assert.Equal(t, "span1", collector.GetZipkinSpans()[0].Name) + + // agentReporter must emit metrics + checkCounters(t, metricsFactory, 1, 1, 0, 0, "zipkin") +} + +func TestZipkinTCollectorReporterFailure(t *testing.T) { + metricsFactory, collector, reporter := initRequirements(t) + defer collector.Close() + + collector.ReturnErr = true + + require.Error(t, submitTestZipkinBatch(reporter)) + + checkCounters(t, metricsFactory, 0, 0, 1, 1, "zipkin") +} + +func submitTestZipkinBatch(reporter Reporter) error { + span := zipkincore.NewSpan() + span.Name = "span1" + + return reporter.EmitZipkinBatch([]*zipkincore.Span{span}) +} + +func TestJaegerTCollectorReporterSuccess(t *testing.T) { + metricsFactory, collector, reporter := initRequirements(t) + defer collector.Close() + + require.NoError(t, submitTestJaegerBatch(reporter)) + + time.Sleep(100 * time.Millisecond) // wait for server to receive + + require.Equal(t, 1, len(collector.GetJaegerBatches())) + assert.Equal(t, "span1", collector.GetJaegerBatches()[0].Spans[0].OperationName) + + // agentReporter must emit metrics + checkCounters(t, metricsFactory, 1, 1, 0, 0, "jaeger") +} + +func TestJaegerTCollectorReporterFailure(t *testing.T) { + metricsFactory, collector, reporter := initRequirements(t) + defer collector.Close() + + collector.ReturnErr = true + + require.Error(t, submitTestJaegerBatch(reporter)) + + // agentReporter must emit metrics + checkCounters(t, metricsFactory, 0, 0, 1, 1, "jaeger") +} + +func submitTestJaegerBatch(reporter Reporter) error { + batch := jaeger.NewBatch() + batch.Process = jaeger.NewProcess() + batch.Spans = []*jaeger.Span{{OperationName: "span1"}} + + return reporter.EmitBatch(batch) +} + +func checkCounters(t *testing.T, mf *metrics.LocalFactory, batchesSubmitted, spansSubmitted, batchesFailures, spansFailures int, prefix string) { + batchesCounter := fmt.Sprintf("tc-reporter.%s.batches.submitted", prefix) + batchesFailureCounter := fmt.Sprintf("tc-reporter.%s.batches.failures", prefix) + spansCounter := fmt.Sprintf("tc-reporter.%s.spans.submitted", prefix) + spansFailureCounter := fmt.Sprintf("tc-reporter.%s.spans.failures", prefix) + + mTestutils.AssertCounterMetrics(t, mf, []mTestutils.ExpectedMetric{ + {Name: batchesCounter, Value: batchesSubmitted}, + {Name: spansCounter, Value: spansSubmitted}, + {Name: batchesFailureCounter, Value: batchesFailures}, + {Name: spansFailureCounter, Value: spansFailures}, + }...) +} diff --git a/cmd/agent/app/sampling/manager.go b/cmd/agent/app/sampling/manager.go new file mode 100644 index 00000000000..a5ec1e430e7 --- /dev/null +++ b/cmd/agent/app/sampling/manager.go @@ -0,0 +1,30 @@ +// Copyright (c) 2017 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. + +package sampling + +import ( + "github.com/uber/jaeger/thrift-gen/sampling" +) + +// Manager decides which sampling strategy a given service should be using. +type Manager interface { + GetSamplingStrategy(serviceName string) (*sampling.SamplingStrategyResponse, error) +} diff --git a/cmd/agent/app/sampling/server.go b/cmd/agent/app/sampling/server.go new file mode 100644 index 00000000000..221a7ce90ce --- /dev/null +++ b/cmd/agent/app/sampling/server.go @@ -0,0 +1,144 @@ +// Copyright (c) 2017 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. + +package sampling + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/uber/jaeger-lib/metrics" + + tSampling "github.com/uber/jaeger/thrift-gen/sampling" +) + +const mimeTypeApplicationJSON = "application/json" + +// NewSamplingServer creates a new server that hosts an HTTP/JSON endpoint for clients +// to query for sampling strategies. +func NewSamplingServer(hostPort string, manager Manager, mFactory metrics.Factory) *http.Server { + handler := newSamplingHandler(manager, mFactory) + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + handler.serveHTTP(w, r, true /* thriftEnums092 */) + }) + mux.HandleFunc("/sampling", func(w http.ResponseWriter, r *http.Request) { + handler.serveHTTP(w, r, false /* thriftEnums092 */) + }) + return &http.Server{Addr: hostPort, Handler: mux} +} + +func newSamplingHandler(manager Manager, mFactory metrics.Factory) *samplingHandler { + handler := &samplingHandler{manager: manager} + metrics.Init(&handler.metrics, mFactory, nil) + return handler +} + +type samplingHandler struct { + manager Manager + metrics struct { + // Number of good sampling requests + GoodRequest metrics.Counter `metric:"sampling-server.requests"` + + // Number of good sampling requests against the old endpoint / using Thrift 0.9.2 enum codes + LegacyRequestThrift092 metrics.Counter `metric:"sampling-server.requests-thrift-092"` + + // Number of bad sampling requests + BadRequest metrics.Counter `metric:"sampling-server.bad-requests"` + + // Number of bad server responses + BadServerResponse metrics.Counter `metric:"sampling-server.bad-server-responses"` + + // Number of bad sampling requests due to malformed thrift + BadThrift metrics.Counter `metric:"sampling-server.bad-thrift"` + + // Number of failed response writes from sampling server + WriteError metrics.Counter `metric:"sampling-server.write-errors"` + } +} + +func (h *samplingHandler) serveHTTP(w http.ResponseWriter, r *http.Request, thriftEnums092 bool) { + services := r.URL.Query()["service"] + if len(services) == 0 { + h.metrics.BadRequest.Inc(1) + http.Error(w, "'service' parameter is empty", http.StatusBadRequest) + return + } + if len(services) > 1 { + h.metrics.BadRequest.Inc(1) + http.Error(w, "'service' parameter must occur only once", http.StatusBadRequest) + return + } + resp, err := h.manager.GetSamplingStrategy(services[0]) + if err != nil { + h.metrics.BadServerResponse.Inc(1) + http.Error(w, fmt.Sprintf("tcollector error: %+v", err), http.StatusInternalServerError) + return + } + json, err := json.Marshal(resp) + if err != nil { + h.metrics.BadThrift.Inc(1) + http.Error(w, "Cannot marshall Thrift to JSON", http.StatusInternalServerError) + return + } + if thriftEnums092 { + json = h.encodeThriftEnums092(json) + } + w.Header().Add("Content-Type", mimeTypeApplicationJSON) + if _, err := w.Write(json); err != nil { + h.metrics.WriteError.Inc(1) + return + } + if thriftEnums092 { + h.metrics.LegacyRequestThrift092.Inc(1) + } else { + h.metrics.GoodRequest.Inc(1) + } +} + +var samplingStrategyTypes = []tSampling.SamplingStrategyType{ + tSampling.SamplingStrategyType_PROBABILISTIC, + tSampling.SamplingStrategyType_RATE_LIMITING, +} + +// Replace string enum values produced from Thrift 0.9.3 generated classes +// with integer codes produced from Thrift 0.9.2 generated classes. +// +// For example: +// +// Thrift 0.9.2 classes generate this JSON: +// {"strategyType":0,"probabilisticSampling":{"samplingRate":0.5},"rateLimitingSampling":null,"operationSampling":null} +// +// Thrift 0.9.3 classes generate this JSON: +// {"strategyType":"PROBABILISTIC","probabilisticSampling":{"samplingRate":0.5}} +func (h *samplingHandler) encodeThriftEnums092(json []byte) []byte { + str := string(json) + for _, strategyType := range samplingStrategyTypes { + str = strings.Replace( + str, + fmt.Sprintf(`"strategyType":"%s"`, strategyType.String()), + fmt.Sprintf(`"strategyType":%d`, strategyType), + 1, + ) + } + return []byte(str) +} diff --git a/cmd/agent/app/sampling/server_test.go b/cmd/agent/app/sampling/server_test.go new file mode 100644 index 00000000000..990f9e12619 --- /dev/null +++ b/cmd/agent/app/sampling/server_test.go @@ -0,0 +1,213 @@ +// Copyright (c) 2017 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. + +package sampling + +import ( + "encoding/json" + "errors" + "io/ioutil" + "math" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/uber/jaeger-lib/metrics" + "github.com/uber/jaeger-lib/metrics/testutils" + mTestutils "github.com/uber/jaeger-lib/metrics/testutils" + "github.com/uber/jaeger/thrift-gen/sampling" + + tSampling092 "github.com/uber/jaeger/cmd/agent/app/sampling/thrift-0.9.2" +) + +type testServer struct { + metricsFactory *metrics.LocalFactory + mgr *mockManager + server *httptest.Server +} + +func withServer( + mockResponse *sampling.SamplingStrategyResponse, + runTest func(server *testServer), +) { + metricsFactory := metrics.NewLocalFactory(0) + mgr := &mockManager{response: mockResponse} + realServer := NewSamplingServer(":0", mgr, metricsFactory) + server := httptest.NewServer(realServer.Handler) + defer server.Close() + runTest(&testServer{ + metricsFactory: metricsFactory, + mgr: mgr, + server: server, + }) +} + +func TestSamplingHandler(t *testing.T) { + withServer(probabilistic(0.001), func(ts *testServer) { + for _, endpoint := range []string{"/", "/sampling"} { + t.Run("request against endpoint "+endpoint, func(t *testing.T) { + resp, err := http.Get(ts.server.URL + endpoint + "?service=Y") + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + if endpoint == "/" { + objResp := &tSampling092.SamplingStrategyResponse{} + if assert.NoError(t, json.Unmarshal(body, objResp)) { + assert.EqualValues(t, + ts.mgr.response.GetStrategyType(), + objResp.GetStrategyType()) + assert.Equal(t, + ts.mgr.response.GetProbabilisticSampling().GetSamplingRate(), + objResp.GetProbabilisticSampling().GetSamplingRate(), + ) + } + } else { + objResp := &sampling.SamplingStrategyResponse{} + if assert.NoError(t, json.Unmarshal(body, objResp)) { + assert.EqualValues(t, ts.mgr.response, objResp) + } + } + }) + } + + // handler must emit metrics + testutils.AssertCounterMetrics(t, ts.metricsFactory, []testutils.ExpectedMetric{ + {Name: "sampling-server.requests", Value: 1}, + {Name: "sampling-server.requests-thrift-092", Value: 1}, + }...) + }) +} + +func TestSamplingHandlerErrors(t *testing.T) { + testCases := []struct { + description string + mockResponse *sampling.SamplingStrategyResponse + url string + statusCode int + body string + metrics []mTestutils.ExpectedMetric + }{ + { + description: "no service name", + url: "", + statusCode: http.StatusBadRequest, + body: "'service' parameter is empty\n", + metrics: []mTestutils.ExpectedMetric{ + {Name: "sampling-server.bad-requests", Value: 1}, + }, + }, + { + description: "too many service names", + url: "?service=Y&service=Y", + statusCode: http.StatusBadRequest, + body: "'service' parameter must occur only once\n", + metrics: []mTestutils.ExpectedMetric{ + {Name: "sampling-server.bad-requests", Value: 1}, + }, + }, + { + description: "tcollector error", + url: "?service=Y", + statusCode: http.StatusInternalServerError, + body: "tcollector error: no mock response provided\n", + metrics: []mTestutils.ExpectedMetric{ + {Name: "sampling-server.bad-server-responses", Value: 1}, + }, + }, + { + description: "marshalling error", + mockResponse: probabilistic(math.NaN()), + url: "?service=Y", + statusCode: http.StatusInternalServerError, + body: "Cannot marshall Thrift to JSON\n", + metrics: []mTestutils.ExpectedMetric{ + {Name: "sampling-server.bad-thrift", Value: 1}, + }, + }, + } + for _, tc := range testCases { + testCase := tc // capture loop var + t.Run(testCase.description, func(t *testing.T) { + withServer(testCase.mockResponse, func(ts *testServer) { + resp, err := http.Get(ts.server.URL + testCase.url) + assert.NoError(t, err) + assert.Equal(t, testCase.statusCode, resp.StatusCode) + if testCase.body != "" { + body, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, testCase.body, string(body)) + } + + if len(testCase.metrics) > 0 { + mTestutils.AssertCounterMetrics(t, ts.metricsFactory, testCase.metrics...) + } + }) + }) + } + + t.Run("failure to write a response", func(t *testing.T) { + withServer(probabilistic(0.001), func(ts *testServer) { + handler := newSamplingHandler(ts.mgr, ts.metricsFactory) + + req := httptest.NewRequest("GET", "http://localhost:80/?service=X", nil) + w := &mockWriter{header: make(http.Header)} + handler.serveHTTP(w, req, false) + + mTestutils.AssertCounterMetrics(t, ts.metricsFactory, + mTestutils.ExpectedMetric{Name: "sampling-server.write-errors", Value: 1}) + }) + }) +} + +func probabilistic(probability float64) *sampling.SamplingStrategyResponse { + return &sampling.SamplingStrategyResponse{ + StrategyType: sampling.SamplingStrategyType_PROBABILISTIC, + ProbabilisticSampling: &sampling.ProbabilisticSamplingStrategy{ + SamplingRate: probability, + }, + } +} + +type mockWriter struct { + header http.Header +} + +func (w *mockWriter) Header() http.Header { + return w.header +} + +func (w *mockWriter) Write([]byte) (int, error) { + return 0, errors.New("write error") +} + +func (w *mockWriter) WriteHeader(int) {} + +type mockManager struct { + response *sampling.SamplingStrategyResponse +} + +func (m *mockManager) GetSamplingStrategy(serviceName string) (*sampling.SamplingStrategyResponse, error) { + if m.response == nil { + return nil, errors.New("no mock response provided") + } + return m.response, nil +} diff --git a/cmd/agent/app/sampling/tcollector_proxy.go b/cmd/agent/app/sampling/tcollector_proxy.go new file mode 100644 index 00000000000..c4ac1f97d72 --- /dev/null +++ b/cmd/agent/app/sampling/tcollector_proxy.go @@ -0,0 +1,64 @@ +// Copyright (c) 2017 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. + +package sampling + +import ( + "time" + + "github.com/uber/jaeger-lib/metrics" + "github.com/uber/tchannel-go" + "github.com/uber/tchannel-go/thrift" + + "github.com/uber/jaeger/thrift-gen/sampling" +) + +type tcollectorSamplingProxy struct { + client sampling.TChanSamplingManager + metrics struct { + // Number of successful sampling rate responses from collector + SamplingResponses metrics.Counter `metric:"tc-sampling-proxy.sampling.responses"` + + // Number of failed sampling rate responses from collector + SamplingErrors metrics.Counter `metric:"tc-sampling-proxy.sampling.errors"` + } +} + +// NewTCollectorSamplingManagerProxy implements Manager by proxying the requests to tcollector. +func NewTCollectorSamplingManagerProxy(channel *tchannel.Channel, mFactory metrics.Factory, clientOpts *thrift.ClientOptions) Manager { + thriftClient := thrift.NewClient(channel, "tcollector", clientOpts) + client := sampling.NewTChanSamplingManagerClient(thriftClient) + res := &tcollectorSamplingProxy{client: client} + metrics.Init(&res.metrics, mFactory, nil) + return res +} + +func (c *tcollectorSamplingProxy) GetSamplingStrategy(serviceName string) (*sampling.SamplingStrategyResponse, error) { + ctx, cancel := tchannel.NewContextBuilder(time.Second).DisableTracing().Build() + defer cancel() + + resp, err := c.client.GetSamplingStrategy(ctx, serviceName) + if err != nil { + c.metrics.SamplingErrors.Inc(1) + return nil, err + } + c.metrics.SamplingResponses.Inc(1) + return resp, nil +} diff --git a/cmd/agent/app/sampling/tcollector_proxy_test.go b/cmd/agent/app/sampling/tcollector_proxy_test.go new file mode 100644 index 00000000000..ea869cdade4 --- /dev/null +++ b/cmd/agent/app/sampling/tcollector_proxy_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2017 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. + +package sampling + +import ( + "errors" + "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/thrift-gen/sampling" + "github.com/uber/tchannel-go/thrift" + + "github.com/uber/jaeger/cmd/agent/app/testutils" +) + +func TestTCollectorProxy(t *testing.T) { + metricsFactory, collector := testutils.InitMockCollector(t) + defer collector.Close() + + collector.AddSamplingStrategy("service1", &sampling.SamplingStrategyResponse{ + StrategyType: sampling.SamplingStrategyType_RATE_LIMITING, + RateLimitingSampling: &sampling.RateLimitingSamplingStrategy{ + MaxTracesPerSecond: 10, + }}) + + mgr := NewTCollectorSamplingManagerProxy(collector.Channel, metricsFactory, nil) + + resp, err := mgr.GetSamplingStrategy("service1") + require.NoError(t, err) + require.NotNil(t, resp) + require.EqualValues(t, resp.StrategyType, sampling.SamplingStrategyType_RATE_LIMITING) + require.NotNil(t, resp.RateLimitingSampling) + require.EqualValues(t, 10, resp.RateLimitingSampling.MaxTracesPerSecond) + + // must emit metrics + mTestutils.AssertCounterMetrics(t, metricsFactory, []mTestutils.ExpectedMetric{ + {Name: "tc-sampling-proxy.sampling.responses", Value: 1}, + {Name: "tc-sampling-proxy.sampling.errors", Value: 0}, + }...) +} + +func TestTCollectorProxyClientErrorPropagates(t *testing.T) { + mFactory := metrics.NewLocalFactory(time.Minute) + client := &failingClient{} + proxy := &tcollectorSamplingProxy{client: client} + metrics.Init(&proxy.metrics, mFactory, nil) + _, err := proxy.GetSamplingStrategy("test") + assert.EqualError(t, err, "error") + mTestutils.AssertCounterMetrics(t, mFactory, + mTestutils.ExpectedMetric{Name: "tc-sampling-proxy.sampling.errors", Value: 1}) +} + +type failingClient struct{} + +func (c *failingClient) GetSamplingStrategy(ctx thrift.Context, serviceName string) (*sampling.SamplingStrategyResponse, error) { + return nil, errors.New("error") +} diff --git a/cmd/agent/app/sampling/thrift-0.9.2/constants.go b/cmd/agent/app/sampling/thrift-0.9.2/constants.go new file mode 100644 index 00000000000..3439274f2aa --- /dev/null +++ b/cmd/agent/app/sampling/thrift-0.9.2/constants.go @@ -0,0 +1,38 @@ +// Copyright (c) 2017 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. + +// Autogenerated by Thrift Compiler (0.9.2) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package sampling + +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() { +} diff --git a/cmd/agent/app/sampling/thrift-0.9.2/ttypes.go b/cmd/agent/app/sampling/thrift-0.9.2/ttypes.go new file mode 100644 index 00000000000..bd234b21c87 --- /dev/null +++ b/cmd/agent/app/sampling/thrift-0.9.2/ttypes.go @@ -0,0 +1,768 @@ +// Copyright (c) 2017 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. + +// Autogenerated by Thrift Compiler (0.9.2) +// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING + +package sampling + +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 + +var GoUnusedProtection__ int + +type SamplingStrategyType int64 + +const ( + SamplingStrategyType_PROBABILISTIC SamplingStrategyType = 0 + SamplingStrategyType_RATE_LIMITING SamplingStrategyType = 1 +) + +func (p SamplingStrategyType) String() string { + switch p { + case SamplingStrategyType_PROBABILISTIC: + return "SamplingStrategyType_PROBABILISTIC" + case SamplingStrategyType_RATE_LIMITING: + return "SamplingStrategyType_RATE_LIMITING" + } + return "" +} + +func SamplingStrategyTypeFromString(s string) (SamplingStrategyType, error) { + switch s { + case "SamplingStrategyType_PROBABILISTIC": + return SamplingStrategyType_PROBABILISTIC, nil + case "SamplingStrategyType_RATE_LIMITING": + return SamplingStrategyType_RATE_LIMITING, nil + } + return SamplingStrategyType(0), fmt.Errorf("not a valid SamplingStrategyType string") +} + +func SamplingStrategyTypePtr(v SamplingStrategyType) *SamplingStrategyType { return &v } + +type ProbabilisticSamplingStrategy struct { + SamplingRate float64 `thrift:"samplingRate,1,required" json:"samplingRate"` +} + +func NewProbabilisticSamplingStrategy() *ProbabilisticSamplingStrategy { + return &ProbabilisticSamplingStrategy{} +} + +func (p *ProbabilisticSamplingStrategy) GetSamplingRate() float64 { + return p.SamplingRate +} +func (p *ProbabilisticSamplingStrategy) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return fmt.Errorf("%T read error: %s", p, err) + } + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return fmt.Errorf("%T field %d read error: %s", 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 fmt.Errorf("%T read struct end error: %s", p, err) + } + return nil +} + +func (p *ProbabilisticSamplingStrategy) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadDouble(); err != nil { + return fmt.Errorf("error reading field 1: %s", err) + } else { + p.SamplingRate = v + } + return nil +} + +func (p *ProbabilisticSamplingStrategy) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("ProbabilisticSamplingStrategy"); err != nil { + return fmt.Errorf("%T write struct begin error: %s", p, err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return fmt.Errorf("write field stop error: %s", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return fmt.Errorf("write struct stop error: %s", err) + } + return nil +} + +func (p *ProbabilisticSamplingStrategy) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("samplingRate", thrift.DOUBLE, 1); err != nil { + return fmt.Errorf("%T write field begin error 1:samplingRate: %s", p, err) + } + if err := oprot.WriteDouble(float64(p.SamplingRate)); err != nil { + return fmt.Errorf("%T.samplingRate (1) field write error: %s", p, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 1:samplingRate: %s", p, err) + } + return err +} + +func (p *ProbabilisticSamplingStrategy) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("ProbabilisticSamplingStrategy(%+v)", *p) +} + +type RateLimitingSamplingStrategy struct { + MaxTracesPerSecond int16 `thrift:"maxTracesPerSecond,1,required" json:"maxTracesPerSecond"` +} + +func NewRateLimitingSamplingStrategy() *RateLimitingSamplingStrategy { + return &RateLimitingSamplingStrategy{} +} + +func (p *RateLimitingSamplingStrategy) GetMaxTracesPerSecond() int16 { + return p.MaxTracesPerSecond +} +func (p *RateLimitingSamplingStrategy) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return fmt.Errorf("%T read error: %s", p, err) + } + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return fmt.Errorf("%T field %d read error: %s", 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 fmt.Errorf("%T read struct end error: %s", p, err) + } + return nil +} + +func (p *RateLimitingSamplingStrategy) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI16(); err != nil { + return fmt.Errorf("error reading field 1: %s", err) + } else { + p.MaxTracesPerSecond = v + } + return nil +} + +func (p *RateLimitingSamplingStrategy) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("RateLimitingSamplingStrategy"); err != nil { + return fmt.Errorf("%T write struct begin error: %s", p, err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return fmt.Errorf("write field stop error: %s", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return fmt.Errorf("write struct stop error: %s", err) + } + return nil +} + +func (p *RateLimitingSamplingStrategy) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("maxTracesPerSecond", thrift.I16, 1); err != nil { + return fmt.Errorf("%T write field begin error 1:maxTracesPerSecond: %s", p, err) + } + if err := oprot.WriteI16(int16(p.MaxTracesPerSecond)); err != nil { + return fmt.Errorf("%T.maxTracesPerSecond (1) field write error: %s", p, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 1:maxTracesPerSecond: %s", p, err) + } + return err +} + +func (p *RateLimitingSamplingStrategy) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("RateLimitingSamplingStrategy(%+v)", *p) +} + +type OperationSamplingStrategy struct { + Operation string `thrift:"operation,1,required" json:"operation"` + ProbabilisticSampling *ProbabilisticSamplingStrategy `thrift:"probabilisticSampling,2,required" json:"probabilisticSampling"` +} + +func NewOperationSamplingStrategy() *OperationSamplingStrategy { + return &OperationSamplingStrategy{} +} + +func (p *OperationSamplingStrategy) GetOperation() string { + return p.Operation +} + +var OperationSamplingStrategy_ProbabilisticSampling_DEFAULT *ProbabilisticSamplingStrategy + +func (p *OperationSamplingStrategy) GetProbabilisticSampling() *ProbabilisticSamplingStrategy { + if !p.IsSetProbabilisticSampling() { + return OperationSamplingStrategy_ProbabilisticSampling_DEFAULT + } + return p.ProbabilisticSampling +} +func (p *OperationSamplingStrategy) IsSetProbabilisticSampling() bool { + return p.ProbabilisticSampling != nil +} + +func (p *OperationSamplingStrategy) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return fmt.Errorf("%T read error: %s", p, err) + } + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return fmt.Errorf("%T field %d read error: %s", p, fieldId, err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if err := p.ReadField1(iprot); err != nil { + return err + } + case 2: + if err := p.ReadField2(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 fmt.Errorf("%T read struct end error: %s", p, err) + } + return nil +} + +func (p *OperationSamplingStrategy) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return fmt.Errorf("error reading field 1: %s", err) + } else { + p.Operation = v + } + return nil +} + +func (p *OperationSamplingStrategy) ReadField2(iprot thrift.TProtocol) error { + p.ProbabilisticSampling = &ProbabilisticSamplingStrategy{} + if err := p.ProbabilisticSampling.Read(iprot); err != nil { + return fmt.Errorf("%T error reading struct: %s", p.ProbabilisticSampling, err) + } + return nil +} + +func (p *OperationSamplingStrategy) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("OperationSamplingStrategy"); err != nil { + return fmt.Errorf("%T write struct begin error: %s", p, err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return fmt.Errorf("write field stop error: %s", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return fmt.Errorf("write struct stop error: %s", err) + } + return nil +} + +func (p *OperationSamplingStrategy) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("operation", thrift.STRING, 1); err != nil { + return fmt.Errorf("%T write field begin error 1:operation: %s", p, err) + } + if err := oprot.WriteString(string(p.Operation)); err != nil { + return fmt.Errorf("%T.operation (1) field write error: %s", p, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 1:operation: %s", p, err) + } + return err +} + +func (p *OperationSamplingStrategy) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("probabilisticSampling", thrift.STRUCT, 2); err != nil { + return fmt.Errorf("%T write field begin error 2:probabilisticSampling: %s", p, err) + } + if err := p.ProbabilisticSampling.Write(oprot); err != nil { + return fmt.Errorf("%T error writing struct: %s", p.ProbabilisticSampling, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 2:probabilisticSampling: %s", p, err) + } + return err +} + +func (p *OperationSamplingStrategy) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("OperationSamplingStrategy(%+v)", *p) +} + +type PerOperationSamplingStrategies struct { + DefaultSamplingProbability float64 `thrift:"defaultSamplingProbability,1,required" json:"defaultSamplingProbability"` + DefaultLowerBoundTracesPerSecond float64 `thrift:"defaultLowerBoundTracesPerSecond,2,required" json:"defaultLowerBoundTracesPerSecond"` + PerOperationStrategies []*OperationSamplingStrategy `thrift:"perOperationStrategies,3,required" json:"perOperationStrategies"` +} + +func NewPerOperationSamplingStrategies() *PerOperationSamplingStrategies { + return &PerOperationSamplingStrategies{} +} + +func (p *PerOperationSamplingStrategies) GetDefaultSamplingProbability() float64 { + return p.DefaultSamplingProbability +} + +func (p *PerOperationSamplingStrategies) GetDefaultLowerBoundTracesPerSecond() float64 { + return p.DefaultLowerBoundTracesPerSecond +} + +func (p *PerOperationSamplingStrategies) GetPerOperationStrategies() []*OperationSamplingStrategy { + return p.PerOperationStrategies +} +func (p *PerOperationSamplingStrategies) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return fmt.Errorf("%T read error: %s", p, err) + } + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return fmt.Errorf("%T field %d read error: %s", p, fieldId, err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if err := p.ReadField1(iprot); err != nil { + return err + } + case 2: + if err := p.ReadField2(iprot); err != nil { + return err + } + case 3: + if err := p.ReadField3(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 fmt.Errorf("%T read struct end error: %s", p, err) + } + return nil +} + +func (p *PerOperationSamplingStrategies) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadDouble(); err != nil { + return fmt.Errorf("error reading field 1: %s", err) + } else { + p.DefaultSamplingProbability = v + } + return nil +} + +func (p *PerOperationSamplingStrategies) ReadField2(iprot thrift.TProtocol) error { + if v, err := iprot.ReadDouble(); err != nil { + return fmt.Errorf("error reading field 2: %s", err) + } else { + p.DefaultLowerBoundTracesPerSecond = v + } + return nil +} + +func (p *PerOperationSamplingStrategies) ReadField3(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return fmt.Errorf("error reading list begin: %s", err) + } + tSlice := make([]*OperationSamplingStrategy, 0, size) + p.PerOperationStrategies = tSlice + for i := 0; i < size; i++ { + _elem0 := &OperationSamplingStrategy{} + if err := _elem0.Read(iprot); err != nil { + return fmt.Errorf("%T error reading struct: %s", _elem0, err) + } + p.PerOperationStrategies = append(p.PerOperationStrategies, _elem0) + } + if err := iprot.ReadListEnd(); err != nil { + return fmt.Errorf("error reading list end: %s", err) + } + return nil +} + +func (p *PerOperationSamplingStrategies) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("PerOperationSamplingStrategies"); err != nil { + return fmt.Errorf("%T write struct begin error: %s", p, err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + if err := p.writeField3(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return fmt.Errorf("write field stop error: %s", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return fmt.Errorf("write struct stop error: %s", err) + } + return nil +} + +func (p *PerOperationSamplingStrategies) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("defaultSamplingProbability", thrift.DOUBLE, 1); err != nil { + return fmt.Errorf("%T write field begin error 1:defaultSamplingProbability: %s", p, err) + } + if err := oprot.WriteDouble(float64(p.DefaultSamplingProbability)); err != nil { + return fmt.Errorf("%T.defaultSamplingProbability (1) field write error: %s", p, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 1:defaultSamplingProbability: %s", p, err) + } + return err +} + +func (p *PerOperationSamplingStrategies) writeField2(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("defaultLowerBoundTracesPerSecond", thrift.DOUBLE, 2); err != nil { + return fmt.Errorf("%T write field begin error 2:defaultLowerBoundTracesPerSecond: %s", p, err) + } + if err := oprot.WriteDouble(float64(p.DefaultLowerBoundTracesPerSecond)); err != nil { + return fmt.Errorf("%T.defaultLowerBoundTracesPerSecond (2) field write error: %s", p, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 2:defaultLowerBoundTracesPerSecond: %s", p, err) + } + return err +} + +func (p *PerOperationSamplingStrategies) writeField3(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("perOperationStrategies", thrift.LIST, 3); err != nil { + return fmt.Errorf("%T write field begin error 3:perOperationStrategies: %s", p, err) + } + if err := oprot.WriteListBegin(thrift.STRUCT, len(p.PerOperationStrategies)); err != nil { + return fmt.Errorf("error writing list begin: %s", err) + } + for _, v := range p.PerOperationStrategies { + if err := v.Write(oprot); err != nil { + return fmt.Errorf("%T error writing struct: %s", v, err) + } + } + if err := oprot.WriteListEnd(); err != nil { + return fmt.Errorf("error writing list end: %s", err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 3:perOperationStrategies: %s", p, err) + } + return err +} + +func (p *PerOperationSamplingStrategies) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("PerOperationSamplingStrategies(%+v)", *p) +} + +type SamplingStrategyResponse struct { + StrategyType SamplingStrategyType `thrift:"strategyType,1,required" json:"strategyType"` + ProbabilisticSampling *ProbabilisticSamplingStrategy `thrift:"probabilisticSampling,2" json:"probabilisticSampling"` + RateLimitingSampling *RateLimitingSamplingStrategy `thrift:"rateLimitingSampling,3" json:"rateLimitingSampling"` + OperationSampling *PerOperationSamplingStrategies `thrift:"operationSampling,4" json:"operationSampling"` +} + +func NewSamplingStrategyResponse() *SamplingStrategyResponse { + return &SamplingStrategyResponse{} +} + +func (p *SamplingStrategyResponse) GetStrategyType() SamplingStrategyType { + return p.StrategyType +} + +var SamplingStrategyResponse_ProbabilisticSampling_DEFAULT *ProbabilisticSamplingStrategy + +func (p *SamplingStrategyResponse) GetProbabilisticSampling() *ProbabilisticSamplingStrategy { + if !p.IsSetProbabilisticSampling() { + return SamplingStrategyResponse_ProbabilisticSampling_DEFAULT + } + return p.ProbabilisticSampling +} + +var SamplingStrategyResponse_RateLimitingSampling_DEFAULT *RateLimitingSamplingStrategy + +func (p *SamplingStrategyResponse) GetRateLimitingSampling() *RateLimitingSamplingStrategy { + if !p.IsSetRateLimitingSampling() { + return SamplingStrategyResponse_RateLimitingSampling_DEFAULT + } + return p.RateLimitingSampling +} + +var SamplingStrategyResponse_OperationSampling_DEFAULT *PerOperationSamplingStrategies + +func (p *SamplingStrategyResponse) GetOperationSampling() *PerOperationSamplingStrategies { + if !p.IsSetOperationSampling() { + return SamplingStrategyResponse_OperationSampling_DEFAULT + } + return p.OperationSampling +} +func (p *SamplingStrategyResponse) IsSetProbabilisticSampling() bool { + return p.ProbabilisticSampling != nil +} + +func (p *SamplingStrategyResponse) IsSetRateLimitingSampling() bool { + return p.RateLimitingSampling != nil +} + +func (p *SamplingStrategyResponse) IsSetOperationSampling() bool { + return p.OperationSampling != nil +} + +func (p *SamplingStrategyResponse) Read(iprot thrift.TProtocol) error { + if _, err := iprot.ReadStructBegin(); err != nil { + return fmt.Errorf("%T read error: %s", p, err) + } + for { + _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() + if err != nil { + return fmt.Errorf("%T field %d read error: %s", p, fieldId, err) + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if err := p.ReadField1(iprot); err != nil { + return err + } + case 2: + if err := p.ReadField2(iprot); err != nil { + return err + } + case 3: + if err := p.ReadField3(iprot); err != nil { + return err + } + case 4: + if err := p.ReadField4(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 fmt.Errorf("%T read struct end error: %s", p, err) + } + return nil +} + +func (p *SamplingStrategyResponse) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadI32(); err != nil { + return fmt.Errorf("error reading field 1: %s", err) + } else { + temp := SamplingStrategyType(v) + p.StrategyType = temp + } + return nil +} + +func (p *SamplingStrategyResponse) ReadField2(iprot thrift.TProtocol) error { + p.ProbabilisticSampling = &ProbabilisticSamplingStrategy{} + if err := p.ProbabilisticSampling.Read(iprot); err != nil { + return fmt.Errorf("%T error reading struct: %s", p.ProbabilisticSampling, err) + } + return nil +} + +func (p *SamplingStrategyResponse) ReadField3(iprot thrift.TProtocol) error { + p.RateLimitingSampling = &RateLimitingSamplingStrategy{} + if err := p.RateLimitingSampling.Read(iprot); err != nil { + return fmt.Errorf("%T error reading struct: %s", p.RateLimitingSampling, err) + } + return nil +} + +func (p *SamplingStrategyResponse) ReadField4(iprot thrift.TProtocol) error { + p.OperationSampling = &PerOperationSamplingStrategies{} + if err := p.OperationSampling.Read(iprot); err != nil { + return fmt.Errorf("%T error reading struct: %s", p.OperationSampling, err) + } + return nil +} + +func (p *SamplingStrategyResponse) Write(oprot thrift.TProtocol) error { + if err := oprot.WriteStructBegin("SamplingStrategyResponse"); err != nil { + return fmt.Errorf("%T write struct begin error: %s", p, err) + } + if err := p.writeField1(oprot); err != nil { + return err + } + if err := p.writeField2(oprot); err != nil { + return err + } + if err := p.writeField3(oprot); err != nil { + return err + } + if err := p.writeField4(oprot); err != nil { + return err + } + if err := oprot.WriteFieldStop(); err != nil { + return fmt.Errorf("write field stop error: %s", err) + } + if err := oprot.WriteStructEnd(); err != nil { + return fmt.Errorf("write struct stop error: %s", err) + } + return nil +} + +func (p *SamplingStrategyResponse) writeField1(oprot thrift.TProtocol) (err error) { + if err := oprot.WriteFieldBegin("strategyType", thrift.I32, 1); err != nil { + return fmt.Errorf("%T write field begin error 1:strategyType: %s", p, err) + } + if err := oprot.WriteI32(int32(p.StrategyType)); err != nil { + return fmt.Errorf("%T.strategyType (1) field write error: %s", p, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 1:strategyType: %s", p, err) + } + return err +} + +func (p *SamplingStrategyResponse) writeField2(oprot thrift.TProtocol) (err error) { + if p.IsSetProbabilisticSampling() { + if err := oprot.WriteFieldBegin("probabilisticSampling", thrift.STRUCT, 2); err != nil { + return fmt.Errorf("%T write field begin error 2:probabilisticSampling: %s", p, err) + } + if err := p.ProbabilisticSampling.Write(oprot); err != nil { + return fmt.Errorf("%T error writing struct: %s", p.ProbabilisticSampling, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 2:probabilisticSampling: %s", p, err) + } + } + return err +} + +func (p *SamplingStrategyResponse) writeField3(oprot thrift.TProtocol) (err error) { + if p.IsSetRateLimitingSampling() { + if err := oprot.WriteFieldBegin("rateLimitingSampling", thrift.STRUCT, 3); err != nil { + return fmt.Errorf("%T write field begin error 3:rateLimitingSampling: %s", p, err) + } + if err := p.RateLimitingSampling.Write(oprot); err != nil { + return fmt.Errorf("%T error writing struct: %s", p.RateLimitingSampling, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 3:rateLimitingSampling: %s", p, err) + } + } + return err +} + +func (p *SamplingStrategyResponse) writeField4(oprot thrift.TProtocol) (err error) { + if p.IsSetOperationSampling() { + if err := oprot.WriteFieldBegin("operationSampling", thrift.STRUCT, 4); err != nil { + return fmt.Errorf("%T write field begin error 4:operationSampling: %s", p, err) + } + if err := p.OperationSampling.Write(oprot); err != nil { + return fmt.Errorf("%T error writing struct: %s", p.OperationSampling, err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return fmt.Errorf("%T write field end error 4:operationSampling: %s", p, err) + } + } + return err +} + +func (p *SamplingStrategyResponse) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("SamplingStrategyResponse(%+v)", *p) +} diff --git a/cmd/agent/app/servers/server.go b/cmd/agent/app/servers/server.go new file mode 100644 index 00000000000..d795a8d3ea0 --- /dev/null +++ b/cmd/agent/app/servers/server.go @@ -0,0 +1,55 @@ +// Copyright (c) 2017 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. + +package servers + +import "io" + +// Server is the interface for servers that receive inbound span submissions from client. +type Server interface { + Serve() + IsServing() bool + Stop() + DataChan() chan *ReadBuf + DataRecd(*ReadBuf) // must be called by consumer after reading data from the ReadBuf +} + +// ReadBuf is a structure that holds the bytes to read into as well as the number of bytes +// that was read. The slice is typically pre-allocated to the max packet size and the buffers +// themselves are polled to avoid memory allocations for every new inbound message. +type ReadBuf struct { + bytes []byte + n int +} + +// GetBytes returns the contents of the Readbuf as bytes +func (r *ReadBuf) GetBytes() []byte { + return r.bytes[:r.n] +} + +func (r *ReadBuf) Read(p []byte) (int, error) { + if r.n == 0 { + return 0, io.EOF + } + n := r.n + copied := copy(p, r.bytes[:n]) + r.n -= copied + return n, nil +} diff --git a/cmd/agent/app/servers/server_test.go b/cmd/agent/app/servers/server_test.go new file mode 100644 index 00000000000..f473b80fb79 --- /dev/null +++ b/cmd/agent/app/servers/server_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2017 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. + +package servers + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadBuf_EOF(t *testing.T) { + b := ReadBuf{} + n, err := b.Read(nil) + assert.Equal(t, 0, n) + assert.Equal(t, io.EOF, err) +} + +func TestReadBuf_Read(t *testing.T) { + b := &ReadBuf{bytes: []byte("hello"), n: 5} + r := make([]byte, 5) + n, err := b.Read(r) + assert.NoError(t, err) + assert.Equal(t, 5, n) + assert.Equal(t, "hello", string(r)) +} diff --git a/cmd/agent/app/servers/tbuffered_server.go b/cmd/agent/app/servers/tbuffered_server.go new file mode 100644 index 00000000000..2dfa2ff494c --- /dev/null +++ b/cmd/agent/app/servers/tbuffered_server.go @@ -0,0 +1,133 @@ +// Copyright (c) 2017 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. + +package servers + +import ( + "sync" + "sync/atomic" + + "github.com/apache/thrift/lib/go/thrift" + "github.com/uber/jaeger-lib/metrics" +) + +// TBufferedServer is a custom thrift server that reads traffic using the transport provided +// and places messages into a buffered channel to be processed by the processor provided +type TBufferedServer struct { + dataChan chan *ReadBuf + maxPacketSize int + maxQueueSize int + queueSize int64 + serving uint32 + transport thrift.TTransport + readBufPool *sync.Pool + metrics struct { + // Size of the current server queue + QueueSize metrics.Gauge `metric:"thrift.udp.server.queue_size"` + + // Size (in bytes) of packets received by server + PacketSize metrics.Gauge `metric:"thrift.udp.server.packet_size"` + + // Number of packets dropped by server + PacketsDropped metrics.Counter `metric:"thrift.udp.server.packets.dropped"` + + // Number of packets processed by server + PacketsProcessed metrics.Counter `metric:"thrift.udp.server.packets.processed"` + + // Number of malformed packets the server received + ReadError metrics.Counter `metric:"thrift.udp.server.read.errors"` + } +} + +// NewTBufferedServer creates a TBufferedServer +func NewTBufferedServer( + transport thrift.TTransport, + maxQueueSize int, + maxPacketSize int, + mFactory metrics.Factory, +) (*TBufferedServer, error) { + dataChan := make(chan *ReadBuf, maxQueueSize) + + var readBufPool = &sync.Pool{ + New: func() interface{} { + return &ReadBuf{bytes: make([]byte, maxPacketSize)} + }, + } + + res := &TBufferedServer{dataChan: dataChan, + transport: transport, + maxQueueSize: maxQueueSize, + maxPacketSize: maxPacketSize, + readBufPool: readBufPool, + } + metrics.Init(&res.metrics, mFactory, nil) + return res, nil +} + +// Serve initiates the readers and starts serving traffic +func (s *TBufferedServer) Serve() { + atomic.StoreUint32(&s.serving, 1) + for s.IsServing() { + readBuf := s.readBufPool.Get().(*ReadBuf) + n, err := s.transport.Read(readBuf.bytes) + if err == nil { + readBuf.n = n + s.metrics.PacketSize.Update(int64(n)) + select { + case s.dataChan <- readBuf: + s.metrics.PacketsProcessed.Inc(1) + s.updateQueueSize(1) + default: + s.metrics.PacketsDropped.Inc(1) + } + } else { + s.metrics.ReadError.Inc(1) + } + } +} + +func (s *TBufferedServer) updateQueueSize(delta int64) { + atomic.AddInt64(&s.queueSize, delta) + s.metrics.QueueSize.Update(atomic.LoadInt64(&s.queueSize)) +} + +// IsServing indicates whether the server is currently serving traffic +func (s *TBufferedServer) IsServing() bool { + return atomic.LoadUint32(&s.serving) == 1 +} + +// Stop stops the serving of traffic and waits until the queue is +// emptied by the readers +func (s *TBufferedServer) Stop() { + atomic.StoreUint32(&s.serving, 0) + s.transport.Close() + close(s.dataChan) +} + +// DataChan returns the data chan of the buffered server +func (s *TBufferedServer) DataChan() chan *ReadBuf { + return s.dataChan +} + +// DataRecd is called by the consumers every time they read a data item from DataChan +func (s *TBufferedServer) DataRecd(buf *ReadBuf) { + s.updateQueueSize(-1) + s.readBufPool.Put(buf) +} diff --git a/cmd/agent/app/servers/tbuffered_server_test.go b/cmd/agent/app/servers/tbuffered_server_test.go new file mode 100644 index 00000000000..106f055af78 --- /dev/null +++ b/cmd/agent/app/servers/tbuffered_server_test.go @@ -0,0 +1,118 @@ +// Copyright (c) 2017 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. + +package servers + +import ( + "testing" + "time" + + athrift "github.com/apache/thrift/lib/go/thrift" + "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/thrift-gen/agent" + "github.com/uber/jaeger/thrift-gen/zipkincore" + + "github.com/uber/jaeger/cmd/agent/app/customtransports" + "github.com/uber/jaeger/cmd/agent/app/servers/thriftudp" + "github.com/uber/jaeger/cmd/agent/app/testutils" +) + +func TestTBufferedServer(t *testing.T) { + t.Run("processed", func(t *testing.T) { + testTBufferedServer(t, 10, false) + }) + t.Run("dropped", func(t *testing.T) { + testTBufferedServer(t, 1, true) + }) +} + +func testTBufferedServer(t *testing.T, queueSize int, testDroppedPackets bool) { + metricsFactory := metrics.NewLocalFactory(0) + + transport, err := thriftudp.NewTUDPServerTransport("127.0.0.1:0") + require.NoError(t, err) + + maxPacketSize := 65000 + server, err := NewTBufferedServer(transport, queueSize, maxPacketSize, metricsFactory) + require.NoError(t, err) + go server.Serve() + defer server.Stop() + time.Sleep(10 * time.Millisecond) // wait for server to start serving + + hostPort := transport.Addr().String() + client, clientCloser, err := testutils.NewZipkinThriftUDPClient(hostPort) + require.NoError(t, err) + defer clientCloser.Close() + + span := zipkincore.NewSpan() + span.Name = "span1" + + err = client.EmitZipkinBatch([]*zipkincore.Span{span}) + require.NoError(t, err) + + if testDroppedPackets { + // because queueSize == 1 for this test, and we're not reading from data chan, + // the second packet we send will be dropped by the server + err = client.EmitZipkinBatch([]*zipkincore.Span{span}) + require.NoError(t, err) + + for i := 0; i < 50; i++ { + c, _ := metricsFactory.Snapshot() + if c["thrift.udp.server.packets.dropped"] == 1 { + return + } + time.Sleep(time.Millisecond) + } + c, _ := metricsFactory.Snapshot() + assert.FailNow(t, "Dropped packets counter not incremented", "Counters: %+v", c) + } + + inMemReporter := testutils.NewInMemoryReporter() + select { + case readBuf := <-server.DataChan(): + assert.NotEqual(t, 0, len(readBuf.GetBytes())) + protoFact := athrift.NewTCompactProtocolFactory() + trans := &customtransport.TBufferedReadTransport{} + protocol := protoFact.GetProtocol(trans) + protocol.Transport().Write(readBuf.GetBytes()) + server.DataRecd(readBuf) + handler := agent.NewAgentProcessor(inMemReporter) + handler.Process(protocol, protocol) + case <-time.After(time.Second * 1): + t.Fatalf("Server should have received span submission") + } + + require.Equal(t, 1, len(inMemReporter.ZipkinSpans())) + assert.Equal(t, "span1", inMemReporter.ZipkinSpans()[0].Name) + + // server must emit metrics + mTestutils.AssertCounterMetrics(t, metricsFactory, + mTestutils.ExpectedMetric{Name: "thrift.udp.server.packets.processed", Value: 1}, + mTestutils.ExpectedMetric{Name: "thrift.udp.server.packets.dropped", Value: 0}, + ) + mTestutils.AssertGaugeMetrics(t, metricsFactory, + mTestutils.ExpectedMetric{Name: "thrift.udp.server.packet_size", Value: 38}, + mTestutils.ExpectedMetric{Name: "thrift.udp.server.queue_size", Value: 0}, + ) +} diff --git a/cmd/agent/app/servers/thriftudp/transport.go b/cmd/agent/app/servers/thriftudp/transport.go new file mode 100644 index 00000000000..9fd005da8ec --- /dev/null +++ b/cmd/agent/app/servers/thriftudp/transport.go @@ -0,0 +1,157 @@ +// Copyright (c) 2017 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. + +package thriftudp + +import ( + "bytes" + "errors" + "net" + "sync/atomic" + + "github.com/apache/thrift/lib/go/thrift" +) + +//MaxLength of UDP packet +const MaxLength = 65000 + +var errConnAlreadyClosed = errors.New("connection already closed") + +// TUDPTransport does UDP as a thrift.TTransport +type TUDPTransport struct { + conn *net.UDPConn + addr net.Addr + writeBuf bytes.Buffer + closed uint32 // atomic flag +} + +// NewTUDPClientTransport creates a net.UDPConn-backed TTransport for Thrift clients +// All writes are buffered and flushed in one UDP packet. If locHostPort is not "", it +// will be used as the local address for the connection +// Example: +// trans, err := thriftudp.NewTUDPClientTransport("192.168.1.1:9090", "") +func NewTUDPClientTransport(destHostPort string, locHostPort string) (*TUDPTransport, error) { + destAddr, err := net.ResolveUDPAddr("udp", destHostPort) + if err != nil { + return nil, thrift.NewTTransportException(thrift.NOT_OPEN, err.Error()) + } + + var locAddr *net.UDPAddr + if locHostPort != "" { + locAddr, err = net.ResolveUDPAddr("udp", locHostPort) + if err != nil { + return nil, thrift.NewTTransportException(thrift.NOT_OPEN, err.Error()) + } + } + + return createClient(destAddr, locAddr) +} + +func createClient(destAddr, locAddr *net.UDPAddr) (*TUDPTransport, error) { + conn, err := net.DialUDP(destAddr.Network(), locAddr, destAddr) + if err != nil { + return nil, thrift.NewTTransportException(thrift.NOT_OPEN, err.Error()) + } + return &TUDPTransport{addr: destAddr, conn: conn}, nil +} + +// 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 := thriftudp.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 atomic.LoadUint32(&p.closed) == 0 +} + +// Close closes the connection +func (p *TUDPTransport) Close() error { + if atomic.CompareAndSwapUint32(&p.closed, 0, 1) { + return p.conn.Close() + } + return errConnAlreadyClosed +} + +// 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 { + const maxSize = ^uint64(0) + return maxSize +} + +// Write writes specified buf to the write buffer +func (p *TUDPTransport) Write(buf []byte) (int, error) { + if !p.IsOpen() { + return 0, thrift.NewTTransportException(thrift.NOT_OPEN, "Connection not open") + } + if len(p.writeBuf.Bytes())+len(buf) > MaxLength { + return 0, thrift.NewTTransportException(thrift.INVALID_DATA, "Data does not fit within one UDP packet") + } + n, err := p.writeBuf.Write(buf) + return n, thrift.NewTTransportExceptionFromError(err) +} + +// Flush flushes the write buffer as one udp packet +func (p *TUDPTransport) Flush() error { + if !p.IsOpen() { + return thrift.NewTTransportException(thrift.NOT_OPEN, "Connection not open") + } + + _, err := p.conn.Write(p.writeBuf.Bytes()) + p.writeBuf.Reset() // always reset the buffer, even in case of an error + return err +} diff --git a/cmd/agent/app/servers/thriftudp/transport_test.go b/cmd/agent/app/servers/thriftudp/transport_test.go new file mode 100644 index 00000000000..0bfbdd6355a --- /dev/null +++ b/cmd/agent/app/servers/thriftudp/transport_test.go @@ -0,0 +1,231 @@ +// Copyright (c) 2017 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. + +package thriftudp + +import ( + "net" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var localListenAddr = &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)} + +func TestNewTUDPClientTransport(t *testing.T) { + _, err := NewTUDPClientTransport("fakeAddressAndPort", "") + require.NotNil(t, err) + + _, err = NewTUDPClientTransport("localhost:9090", "fakeaddressandport") + require.NotNil(t, err) + + withLocalServer(t, func(addr string) { + trans, err := NewTUDPClientTransport(addr, "") + require.Nil(t, err) + require.True(t, trans.IsOpen()) + require.NotNil(t, trans.Addr()) + + //Check address + assert.True(t, strings.HasPrefix(trans.Addr().String(), "127.0.0.1:"), "address check") + require.Equal(t, "udp", trans.Addr().Network()) + + err = trans.Open() + require.Nil(t, err) + + err = trans.Close() + require.Nil(t, err) + require.False(t, trans.IsOpen()) + }) +} + +func TestNewTUDPServerTransport(t *testing.T) { + _, err := NewTUDPServerTransport("fakeAddressAndPort") + require.NotNil(t, err) + + trans, err := NewTUDPServerTransport(localListenAddr.String()) + require.Nil(t, err) + require.True(t, trans.IsOpen()) + require.Equal(t, ^uint64(0), trans.RemainingBytes()) + + //Ensure a second server can't be created on the same address + trans2, err := NewTUDPServerTransport(trans.Addr().String()) + if trans2 != nil { + //close the second server if one got created + trans2.Close() + } + require.NotNil(t, err) + + err = trans.Close() + require.Nil(t, err) + require.False(t, trans.IsOpen()) +} + +func TestTUDPServerTransportIsOpen(t *testing.T) { + _, err := NewTUDPServerTransport("fakeAddressAndPort") + require.NotNil(t, err) + + trans, err := NewTUDPServerTransport(localListenAddr.String()) + require.Nil(t, err) + require.True(t, trans.IsOpen()) + require.Equal(t, ^uint64(0), trans.RemainingBytes()) + + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + time.Sleep(2 * time.Millisecond) + err = trans.Close() + require.Nil(t, err) + wg.Done() + }() + + go func() { + for i := 0; i < 4; i++ { + time.Sleep(1 * time.Millisecond) + trans.IsOpen() + } + wg.Done() + }() + + wg.Wait() + require.False(t, trans.IsOpen()) +} + +func TestWriteRead(t *testing.T) { + server, err := NewTUDPServerTransport(localListenAddr.String()) + require.Nil(t, err) + defer server.Close() + + client, err := NewTUDPClientTransport(server.Addr().String(), "") + require.Nil(t, err) + defer client.Close() + + n, err := client.Write([]byte("test")) + require.Nil(t, err) + require.Equal(t, 4, n) + n, err = client.Write([]byte("string")) + require.Nil(t, err) + require.Equal(t, 6, n) + err = client.Flush() + require.Nil(t, err) + + expected := []byte("teststring") + readBuf := make([]byte, 20) + n, err = server.Read(readBuf) + require.Nil(t, err) + require.Equal(t, len(expected), n) + require.Equal(t, expected, readBuf[0:n]) +} + +func TestDoubleCloseError(t *testing.T) { + trans, err := NewTUDPServerTransport(localListenAddr.String()) + require.Nil(t, err) + require.True(t, trans.IsOpen()) + + //Close connection object directly + conn := trans.Conn() + require.NotNil(t, conn) + conn.Close() + + err = trans.Close() + require.Error(t, err, "must return error when underlying connection is closed") + + assert.Equal(t, errConnAlreadyClosed, trans.Close(), "second Close() returns an error") +} + +func TestConnClosedReadWrite(t *testing.T) { + trans, err := NewTUDPServerTransport(localListenAddr.String()) + require.Nil(t, err) + require.True(t, trans.IsOpen()) + require.NoError(t, trans.Close()) + require.False(t, trans.IsOpen()) + + _, err = trans.Read(make([]byte, 1)) + require.NotNil(t, err) + _, err = trans.Write([]byte("test")) + require.NotNil(t, err) +} + +func TestHugeWrite(t *testing.T) { + withLocalServer(t, func(addr string) { + trans, err := NewTUDPClientTransport(addr, "") + require.Nil(t, err) + + hugeMessage := make([]byte, 40000) + _, err = trans.Write(hugeMessage) + require.Nil(t, err) + + //expect buffer to exceed max + _, err = trans.Write(hugeMessage) + require.NotNil(t, err) + }) +} + +func TestFlushErrors(t *testing.T) { + withLocalServer(t, func(addr string) { + trans, err := NewTUDPClientTransport(addr, "") + require.Nil(t, err) + + //flushing closed transport + trans.Close() + err = trans.Flush() + require.NotNil(t, err) + + //error when trying to write in flush + trans, err = NewTUDPClientTransport(addr, "") + require.Nil(t, err) + trans.conn.Close() + + trans.Write([]byte{1, 2, 3, 4}) + err = trans.Flush() + require.Error(t, trans.Flush(), "Flush with data should fail") + }) +} + +func TestResetInFlush(t *testing.T) { + conn, err := net.ListenUDP(localListenAddr.Network(), localListenAddr) + require.NoError(t, err, "ListenUDP failed") + + trans, err := NewTUDPClientTransport(conn.LocalAddr().String(), "") + require.Nil(t, err) + + trans.Write([]byte("some nonsense")) + trans.conn.Close() // close the transport's connection via back door + + err = trans.Flush() + require.NotNil(t, err, "should fail to write to closed connection") + assert.Equal(t, 0, trans.writeBuf.Len(), "should reset the buffer") +} + +func withLocalServer(t *testing.T, f func(addr string)) { + conn, err := net.ListenUDP(localListenAddr.Network(), localListenAddr) + require.NoError(t, err, "ListenUDP failed") + + f(conn.LocalAddr().String()) + require.NoError(t, conn.Close(), "Close failed") +} + +func TestCreateClient(t *testing.T) { + _, err := createClient(nil, nil) + assert.EqualError(t, err, "dial udp: missing address") +} diff --git a/cmd/agent/app/testdata/test_config.yaml b/cmd/agent/app/testdata/test_config.yaml new file mode 100644 index 00000000000..09247309ec1 --- /dev/null +++ b/cmd/agent/app/testdata/test_config.yaml @@ -0,0 +1,25 @@ +metrics: + statsd: + hostPort: 127.0.0.1:4744 + prefix: jaeger-agent.tcollector21-sjc1 + +processors: + - model: zipkin + protocol: compact + server: + hostPort: 1.1.1.1:5775 + - model: jaeger + protocol: compact + server: + hostPort: 2.2.2.2:6831 + - model: jaeger + protocol: binary + workers: 20 + server: + queueSize: 2000 + maxPacketSize: 65001 + hostPort: 3.3.3.3:6832 + +samplingServer: + hostPort: 4.4.4.4:5778 + diff --git a/cmd/agent/app/testutils/fixture.go b/cmd/agent/app/testutils/fixture.go new file mode 100644 index 00000000000..2621efcb815 --- /dev/null +++ b/cmd/agent/app/testutils/fixture.go @@ -0,0 +1,37 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/uber/jaeger-lib/metrics" +) + +// InitMockCollector initializes a MockTCollector fixture +func InitMockCollector(t *testing.T) (*metrics.LocalFactory, *MockTCollector) { + factory := metrics.NewLocalFactory(0) + collector, err := StartMockTCollector() + require.NoError(t, err) + + return factory, collector +} diff --git a/cmd/agent/app/testutils/in_memory_reporter.go b/cmd/agent/app/testutils/in_memory_reporter.go new file mode 100644 index 00000000000..d858fb36683 --- /dev/null +++ b/cmd/agent/app/testutils/in_memory_reporter.go @@ -0,0 +1,73 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "sync" + + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" +) + +// InMemoryReporter collects spans in memory +type InMemoryReporter struct { + zSpans []*zipkincore.Span + jSpans []*jaeger.Span + mutex sync.Mutex +} + +// NewInMemoryReporter creates new InMemoryReporter +func NewInMemoryReporter() *InMemoryReporter { + return &InMemoryReporter{ + zSpans: make([]*zipkincore.Span, 0, 10), + jSpans: make([]*jaeger.Span, 0, 10), + } +} + +// EmitZipkinBatch implements the corresponding method of the Reporter interface +func (i *InMemoryReporter) EmitZipkinBatch(spans []*zipkincore.Span) error { + i.mutex.Lock() + defer i.mutex.Unlock() + i.zSpans = append(i.zSpans, spans...) + return nil +} + +// EmitBatch implements the corresponding method of the Reporter interface +func (i *InMemoryReporter) EmitBatch(batch *jaeger.Batch) (err error) { + i.mutex.Lock() + defer i.mutex.Unlock() + i.jSpans = append(i.jSpans, batch.Spans...) + return nil +} + +// ZipkinSpans returns accumulated Zipkin spans as a copied slice +func (i *InMemoryReporter) ZipkinSpans() []*zipkincore.Span { + i.mutex.Lock() + defer i.mutex.Unlock() + return i.zSpans[:] +} + +// Spans returns accumulated spans as a copied slice +func (i *InMemoryReporter) Spans() []*jaeger.Span { + i.mutex.Lock() + defer i.mutex.Unlock() + return i.jSpans[:] +} diff --git a/cmd/agent/app/testutils/in_memory_reporter_test.go b/cmd/agent/app/testutils/in_memory_reporter_test.go new file mode 100644 index 00000000000..933b3d85594 --- /dev/null +++ b/cmd/agent/app/testutils/in_memory_reporter_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/zipkincore" +) + +func TestInMemoryReporter(t *testing.T) { + r := NewInMemoryReporter() + e1 := r.EmitZipkinBatch([]*zipkincore.Span{ + {}, + }) + e2 := r.EmitBatch(&jaeger.Batch{ + Spans: []*jaeger.Span{ + {}, + }, + }) + assert.NoError(t, e1) + assert.NoError(t, e2) + assert.Len(t, r.ZipkinSpans(), 1) + assert.Len(t, r.Spans(), 1) +} diff --git a/cmd/agent/app/testutils/mock_collector.go b/cmd/agent/app/testutils/mock_collector.go new file mode 100644 index 00000000000..50aa9d61fbc --- /dev/null +++ b/cmd/agent/app/testutils/mock_collector.go @@ -0,0 +1,152 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "errors" + "sync" + + "github.com/uber/tchannel-go" + "github.com/uber/tchannel-go/thrift" + + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/sampling" + "github.com/uber/jaeger/thrift-gen/zipkincore" +) + +// StartMockTCollector runs a mock representation of Jaeger Collector. +// This function returns a started server, with a Channel that knows +// how to find that server, which can be used in clients or Jaeger tracer. +// +// TODO we should refactor normal collector so it can be used in tests, with in-memory storage +func StartMockTCollector() (*MockTCollector, error) { + return startMockTCollector("tcollector", "127.0.0.1:0") +} + +// extracted to separate function to be able to test error cases +func startMockTCollector(name string, addr string) (*MockTCollector, error) { + ch, err := tchannel.NewChannel(name, nil) + if err != nil { + return nil, err + } + + server := thrift.NewServer(ch) + + collector := &MockTCollector{ + Channel: ch, + server: server, + zipkinSpans: make([]*zipkincore.Span, 0, 10), + jaegerBatches: make([]*jaeger.Batch, 0, 10), + samplingMgr: newSamplingManager(), + ReturnErr: false, + } + + server.Register(zipkincore.NewTChanZipkinCollectorServer(collector)) + server.Register(jaeger.NewTChanCollectorServer(collector)) + server.Register(sampling.NewTChanSamplingManagerServer(&tchanSamplingManager{collector.samplingMgr})) + + if err := ch.ListenAndServe(addr); err != nil { + return nil, err + } + + subchannel := ch.GetSubChannel("tcollector", tchannel.Isolated) + subchannel.Peers().Add(ch.PeerInfo().HostPort) + + return collector, nil +} + +// MockTCollector is a mock representation of Jaeger Collector. +type MockTCollector struct { + Channel *tchannel.Channel + server *thrift.Server + zipkinSpans []*zipkincore.Span + jaegerBatches []*jaeger.Batch + mutex sync.Mutex + samplingMgr *samplingManager + ReturnErr bool +} + +// AddSamplingStrategy registers a sampling strategy for a service +func (s *MockTCollector) AddSamplingStrategy( + service string, + strategy *sampling.SamplingStrategyResponse, +) { + s.samplingMgr.AddSamplingStrategy(service, strategy) +} + +// GetZipkinSpans returns accumulated Zipkin spans +func (s *MockTCollector) GetZipkinSpans() []*zipkincore.Span { + s.mutex.Lock() + defer s.mutex.Unlock() + return s.zipkinSpans[:] +} + +// GetJaegerBatches returns accumulated Jaeger batches +func (s *MockTCollector) GetJaegerBatches() []*jaeger.Batch { + s.mutex.Lock() + defer s.mutex.Unlock() + return s.jaegerBatches[:] +} + +// Close stops/closes the underlying channel and server +func (s *MockTCollector) Close() { + s.Channel.Close() +} + +// SubmitZipkinBatch implements handler method of TChanZipkinCollectorServer +func (s *MockTCollector) SubmitZipkinBatch( + ctx thrift.Context, + spans []*zipkincore.Span, +) ([]*zipkincore.Response, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + if s.ReturnErr { + return []*zipkincore.Response{{Ok: false}}, errors.New("Returning error from MockTCollector") + } + s.zipkinSpans = append(s.zipkinSpans, spans...) + return []*zipkincore.Response{{Ok: true}}, nil +} + +// SubmitBatches implements handler method of TChanCollectorServer +func (s *MockTCollector) SubmitBatches( + ctx thrift.Context, + batches []*jaeger.Batch, +) ([]*jaeger.BatchSubmitResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + if s.ReturnErr { + return []*jaeger.BatchSubmitResponse{{Ok: false}}, errors.New("Returning error from MockTCollector") + } + s.jaegerBatches = append(s.jaegerBatches, batches...) + return []*jaeger.BatchSubmitResponse{{Ok: true}}, nil +} + +type tchanSamplingManager struct { + samplingMgr *samplingManager +} + +// GetSamplingStrategy implements GetSamplingStrategy of TChanSamplingManagerServer +func (s *tchanSamplingManager) GetSamplingStrategy( + ctx thrift.Context, + serviceName string, +) (*sampling.SamplingStrategyResponse, error) { + return s.samplingMgr.GetSamplingStrategy(serviceName) +} diff --git a/cmd/agent/app/testutils/mock_collector_test.go b/cmd/agent/app/testutils/mock_collector_test.go new file mode 100644 index 00000000000..310621d7351 --- /dev/null +++ b/cmd/agent/app/testutils/mock_collector_test.go @@ -0,0 +1,141 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber/tchannel-go/thrift" + + "github.com/uber/jaeger/thrift-gen/jaeger" + "github.com/uber/jaeger/thrift-gen/sampling" + "github.com/uber/jaeger/thrift-gen/zipkincore" +) + +func withTCollector(t *testing.T, fn func(collector *MockTCollector, ctx thrift.Context)) { + _, collector := InitMockCollector(t) + defer collector.Close() + + time.Sleep(10 * time.Millisecond) // give the server a chance to start + + ctx, ctxCancel := thrift.NewContext(time.Second) + defer ctxCancel() + + fn(collector, ctx) +} + +func withSamplingClient(t *testing.T, fn func(collector *MockTCollector, ctx thrift.Context, client sampling.TChanSamplingManager)) { + withTCollector(t, func(collector *MockTCollector, ctx thrift.Context) { + thriftClient := thrift.NewClient(collector.Channel, "tcollector", nil) + client := sampling.NewTChanSamplingManagerClient(thriftClient) + + fn(collector, ctx, client) + }) +} + +func withZipkinClient(t *testing.T, fn func(collector *MockTCollector, ctx thrift.Context, client zipkincore.TChanZipkinCollector)) { + withTCollector(t, func(collector *MockTCollector, ctx thrift.Context) { + thriftClient := thrift.NewClient(collector.Channel, "tcollector", nil) + client := zipkincore.NewTChanZipkinCollectorClient(thriftClient) + + fn(collector, ctx, client) + }) +} + +func withJaegerClient(t *testing.T, fn func(collector *MockTCollector, ctx thrift.Context, client jaeger.TChanCollector)) { + withTCollector(t, func(collector *MockTCollector, ctx thrift.Context) { + thriftClient := thrift.NewClient(collector.Channel, "tcollector", nil) + client := jaeger.NewTChanCollectorClient(thriftClient) + + fn(collector, ctx, client) + }) +} + +func TestMockTCollectorSampling(t *testing.T) { + withSamplingClient(t, func(collector *MockTCollector, ctx thrift.Context, client sampling.TChanSamplingManager) { + s, err := client.GetSamplingStrategy(ctx, "default-service") + require.NoError(t, err) + require.Equal(t, sampling.SamplingStrategyType_PROBABILISTIC, s.StrategyType) + require.NotNil(t, s.ProbabilisticSampling) + assert.Equal(t, 0.01, s.ProbabilisticSampling.SamplingRate) + + collector.AddSamplingStrategy("service1", &sampling.SamplingStrategyResponse{ + StrategyType: sampling.SamplingStrategyType_RATE_LIMITING, + RateLimitingSampling: &sampling.RateLimitingSamplingStrategy{ + MaxTracesPerSecond: 10, + }}) + + s, err = client.GetSamplingStrategy(ctx, "service1") + require.NoError(t, err) + require.Equal(t, sampling.SamplingStrategyType_RATE_LIMITING, s.StrategyType) + require.NotNil(t, s.RateLimitingSampling) + assert.EqualValues(t, 10, s.RateLimitingSampling.MaxTracesPerSecond) + }) +} + +func TestMockTCollectorZipkin(t *testing.T) { + withZipkinClient(t, func(collector *MockTCollector, ctx thrift.Context, client zipkincore.TChanZipkinCollector) { + span := &zipkincore.Span{Name: "service3"} + _, err := client.SubmitZipkinBatch(ctx, []*zipkincore.Span{span}) + require.NoError(t, err) + spans := collector.GetZipkinSpans() + require.Equal(t, 1, len(spans)) + assert.Equal(t, "service3", spans[0].Name) + + collector.ReturnErr = true + _, err = client.SubmitZipkinBatch(ctx, []*zipkincore.Span{span}) + assert.Error(t, err) + }) +} + +func TestMockTCollector(t *testing.T) { + withJaegerClient(t, func(collector *MockTCollector, ctx thrift.Context, client jaeger.TChanCollector) { + batch := &jaeger.Batch{ + Spans: []*jaeger.Span{ + {OperationName: "service4"}, + }, + Process: &jaeger.Process{ + ServiceName: "someServiceName", + }, + } + _, err := client.SubmitBatches(ctx, []*jaeger.Batch{batch}) + require.NoError(t, err) + batches := collector.GetJaegerBatches() + require.Equal(t, 1, len(batches)) + assert.Equal(t, "service4", batches[0].Spans[0].OperationName) + assert.Equal(t, "someServiceName", batches[0].Process.ServiceName) + + collector.ReturnErr = true + _, err = client.SubmitBatches(ctx, []*jaeger.Batch{batch}) + assert.Error(t, err) + }) +} + +func TestMockTCollectorErrors(t *testing.T) { + _, err := startMockTCollector("", "127.0.0.1:0") + assert.Error(t, err, "error because of empty service name") + + _, err = startMockTCollector("test", "127.0.0:0") + assert.Error(t, err, "error because of bad address") +} diff --git a/cmd/agent/app/testutils/mock_sampling_manager.go b/cmd/agent/app/testutils/mock_sampling_manager.go new file mode 100644 index 00000000000..52c908cf23c --- /dev/null +++ b/cmd/agent/app/testutils/mock_sampling_manager.go @@ -0,0 +1,64 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "sync" + + "github.com/uber/jaeger/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 +} diff --git a/cmd/agent/app/testutils/thriftudp_client.go b/cmd/agent/app/testutils/thriftudp_client.go new file mode 100644 index 00000000000..c5eb090e4c6 --- /dev/null +++ b/cmd/agent/app/testutils/thriftudp_client.go @@ -0,0 +1,54 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "io" + + "github.com/apache/thrift/lib/go/thrift" + + "github.com/uber/jaeger/cmd/agent/app/servers/thriftudp" + "github.com/uber/jaeger/thrift-gen/agent" + "github.com/uber/jaeger/thrift-gen/jaeger" +) + +// NewZipkinThriftUDPClient creates a new zipking agent client that works like Jaeger client +func NewZipkinThriftUDPClient(hostPort string) (*agent.AgentClient, io.Closer, error) { + clientTransport, err := thriftudp.NewTUDPClientTransport(hostPort, "") + if err != nil { + return nil, nil, err + } + + protocolFactory := thrift.NewTCompactProtocolFactory() + client := agent.NewAgentClientFactory(clientTransport, protocolFactory) + return client, clientTransport, nil +} + +// NewJaegerThriftUDPClient creates a new jaeger agent client that works like Jaeger client +func NewJaegerThriftUDPClient(hostPort string, protocolFactory thrift.TProtocolFactory) (*jaeger.AgentClient, io.Closer, error) { + clientTransport, err := thriftudp.NewTUDPClientTransport(hostPort, "") + if err != nil { + return nil, nil, err + } + + client := jaeger.NewAgentClientFactory(clientTransport, protocolFactory) + return client, clientTransport, nil +} diff --git a/cmd/agent/app/testutils/thriftudp_client_test.go b/cmd/agent/app/testutils/thriftudp_client_test.go new file mode 100644 index 00000000000..736bd7bdb22 --- /dev/null +++ b/cmd/agent/app/testutils/thriftudp_client_test.go @@ -0,0 +1,49 @@ +// Copyright (c) 2017 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. + +package testutils + +import ( + "testing" + + "github.com/apache/thrift/lib/go/thrift" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewZipkinThriftUDPClient(t *testing.T) { + _, _, err := NewZipkinThriftUDPClient("1.2.3:0") + assert.Error(t, err) + + _, cl, err := NewZipkinThriftUDPClient("127.0.0.1:12345") + require.NoError(t, err) + cl.Close() +} + +func TestNewJaegerThriftUDPClient(t *testing.T) { + compactFactory := thrift.NewTCompactProtocolFactory() + + _, _, err := NewJaegerThriftUDPClient("1.2.3:0", compactFactory) + assert.Error(t, err) + + _, cl, err := NewJaegerThriftUDPClient("127.0.0.1:12345", compactFactory) + require.NoError(t, err) + cl.Close() +} diff --git a/cmd/agent/main.go b/cmd/agent/main.go index cd5ca01f677..924611bbeed 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -20,6 +20,38 @@ package main +import ( + "flag" + "runtime" + + "github.com/uber-go/zap" + "github.com/uber/jaeger-lib/metrics/go-kit" + "github.com/uber/jaeger-lib/metrics/go-kit/expvar" + + "github.com/uber/jaeger/cmd/agent/app" +) + func main() { - println("jaeger-agent") + builder := app.NewBuilder() + builder.Bind(flag.CommandLine) + flag.Parse() + + runtime.GOMAXPROCS(runtime.NumCPU()) + + logger := zap.New(zap.NewJSONEncoder()) + metricsFactory := xkit.Wrap("jaeger-agent", expvar.NewFactory(10)) + + // TODO illustrate discovery service wiring + // TODO illustrate additional reporter + + agent, err := builder.CreateAgent(metricsFactory, logger) + if err != nil { + logger.Fatal("Unable to initialize Jaeger Agent", zap.Error(err)) + } + + logger.Info("Starting agent") + if err := agent.Run(); err != nil { + logger.Fatal("Failed to run the agent", zap.Error(err)) + } + select {} } diff --git a/glide.lock b/glide.lock index 67ac084a49d..ecb4f59b946 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 35bbf06daec5ef20d2f5cf83ab41020bf29c6a0a4ce045e25f755933e8169646 -updated: 2017-02-27T14:28:37.546764236-05:00 +hash: 58c57d356d0af8d48c3a75adb711d86239720fc846a4e26fee2e6304f7408328 +updated: 2017-03-18T20:52:33.113387-04:00 imports: - name: github.com/apache/thrift version: 53dd39833a08ce33582e5ff31fa18bb4735d6731 @@ -11,8 +11,13 @@ imports: version: 346938d642f2ec3594ed81d874461961cd0faa76 subpackages: - spew -- name: github.com/fsnotify/fsnotify - version: 30411dbcefb7a1da7e84f75530ad3abe4011b4f8 +- name: github.com/go-kit/kit + version: fadad6fffe0466b19df9efd9acde5c9a52df5fa4 + subpackages: + - metrics + - metrics/expvar + - metrics/generic + - metrics/internal/lv - name: github.com/gocql/gocql version: 4d2d1ac71932f7c4a6c7feb0d654462e4116c58b subpackages: @@ -29,60 +34,23 @@ imports: version: 392c28fe23e1c45ddba891b0320b3b5df220beea - name: github.com/hailocab/go-hostpool version: e80d13ce29ede4452c43dea11e79b9bc8a15b478 -- name: github.com/hashicorp/hcl - version: 630949a3c5fa3c613328e1b8256052cbc2327c9b - subpackages: - - hcl/ast - - hcl/parser - - hcl/scanner - - hcl/strconv - - hcl/token - - json/parser - - json/scanner - - json/token -- name: github.com/inconshreveable/mousetrap - version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 -- name: github.com/magiconair/properties - version: b3b15ef068fd0b17ddf408a23669f20811d194d2 -- name: github.com/mitchellh/mapstructure - version: db1efb556f84b25a0a13a04aad883943538ad2e0 -- name: github.com/opentracing-contrib/go-stdlib - version: 1de4cc2120e71f745a5810488bf64b29b6d7d9f6 - subpackages: - - nethttp +- name: github.com/kr/pretty + version: cfb55aafdaf3ec08f0db22699ab822c50091b1c4 - name: github.com/opentracing/opentracing-go version: 0c3154a3c2ce79d3271985848659870599dfb77c subpackages: - ext - log -- name: github.com/pelletier/go-buffruneio - version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d -- name: github.com/pelletier/go-toml - version: 22139eb5469018e7374b3e7ef653de37ffb44f72 - name: github.com/pkg/errors version: 248dadf4e9068a0b3e79f02ed0a610d935de5302 - name: github.com/pmezard/go-difflib version: 792786c7400a136282c1664665ae0a8db921c6c2 subpackages: - difflib -- name: github.com/spf13/afero - version: 9be650865eab0c12963d8753212f4f9c66cdcf12 - subpackages: - - mem -- name: github.com/spf13/cast - version: f820543c3592e283e311a60d2a600a664e39f6f7 -- name: github.com/spf13/cobra - version: 92ea23a837e66f46ac9e7d04fa826602b7b0a42d -- name: github.com/spf13/jwalterweatherman - version: fa7ca7e836cf3a8bb4ebf799f472c12d7e903d66 -- name: github.com/spf13/pflag - version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 -- name: github.com/spf13/viper - version: 7538d73b4eb9511d85a9f1dfef202eeb8ac260f4 - name: github.com/stretchr/objx version: 1a9d0bb9f541897e62256577b352fdbc1fb4fd94 - name: github.com/stretchr/testify - version: 6cb3b85ef5a0efef77caef88363ec4d4b5c0976d + version: 4d4bfba8f1d1027c4fdbe371823030df51419987 subpackages: - assert - mock @@ -95,34 +63,32 @@ imports: version: b9556711760c45a30bd79c31c8f041d0d9aba997 subpackages: - metrics + - metrics/go-kit + - metrics/go-kit/expvar + - metrics/testutils - name: github.com/uber/tchannel-go version: 1a0e35378f6f721bc07f6c4466bc9701ed70b506 subpackages: + - raw - relay + - relay/relaytest + - testutils + - testutils/goroutines - thrift - thrift/gen-go/meta - tnet - trand - typed +- name: github.com/VividCortex/gohistogram + version: 51564d9861991fb0ad0f531c99ef602d0f9866e6 - name: golang.org/x/net - version: 3b993948b6f0e651ffb58ba135d8538a68b1cddf + version: a6577fac2d73be281a500b310739095313165611 subpackages: - context -- name: golang.org/x/sys - version: d4feaf1a7e61e1d9e79e6c4e76c6349e9cab0a03 - subpackages: - - unix -- name: golang.org/x/text - version: 0ad425fe45e885577bef05dc1c50f72e33188b16 - subpackages: - - transform - - unicode/norm - name: gopkg.in/inf.v0 version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 -- name: gopkg.in/yaml.v2 - version: a83829b6f1293c91addabc89d0571c246397bbf4 testImports: -- name: github.com/kr/pretty - version: cfb55aafdaf3ec08f0db22699ab822c50091b1c4 - name: github.com/kr/text version: 7cafcd837844e784b526369c9bce262804aebc60 +- name: gopkg.in/yaml.v2 + version: a3f3340b5840cee44f372bddb5880fcbc419b46a diff --git a/glide.yaml b/glide.yaml index 3742c34211e..c91e5bd8bd2 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,7 +22,6 @@ import: version: v1.3.0 - package: github.com/gorilla/handlers version: v1.2 -testImport: - package: github.com/kr/pretty - package: github.com/stretchr/testify subpackages: