opentelemetry-collector/exporter/zipkinexporter/zipkin_test.go

360 lines
10 KiB
Go

// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package zipkinexporter
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
zipkinmodel "github.com/openzipkin/zipkin-go/model"
"github.com/openzipkin/zipkin-go/proto/zipkin_proto3"
zipkinreporter "github.com/openzipkin/zipkin-go/reporter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/receiver/zipkinreceiver"
"go.opentelemetry.io/collector/testutil"
)
// This function tests that Zipkin spans that are received then processed roundtrip
// back to almost the same JSON with differences:
// a) Go's net.IP.String intentional shortens 0s with "::" but also converts to hex values
// so
// "7::0.128.128.127"
// becomes
// "7::80:807f"
//
// The rest of the fields should match up exactly
func TestZipkinExporter_roundtripJSON(t *testing.T) {
buf := new(bytes.Buffer)
var sizes []int64
cst := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s, _ := io.Copy(buf, r.Body)
sizes = append(sizes, s)
r.Body.Close()
}))
defer cst.Close()
cfg := &Config{
HTTPClientSettings: confighttp.HTTPClientSettings{
Endpoint: cst.URL,
},
Format: "json",
}
zexp, err := NewFactory().CreateTracesExporter(context.Background(), component.ExporterCreateParams{Logger: zap.NewNop()}, cfg)
assert.NoError(t, err)
require.NotNil(t, zexp)
// The test requires the spans from zipkinSpansJSONJavaLibrary to be sent in a single batch, use
// a mock to ensure that this happens as intended.
mzr := newMockZipkinReporter(cst.URL)
// Run the Zipkin receiver to "receive spans upload from a client application"
addr := testutil.GetAvailableLocalAddress(t)
recvCfg := &zipkinreceiver.Config{
ReceiverSettings: config.ReceiverSettings{
NameVal: "zipkin_receiver",
},
HTTPServerSettings: confighttp.HTTPServerSettings{
Endpoint: addr,
},
}
zi, err := zipkinreceiver.New(recvCfg, zexp)
assert.NoError(t, err)
require.NotNil(t, zi)
require.NoError(t, zi.Start(context.Background(), componenttest.NewNopHost()))
defer zi.Shutdown(context.Background())
// Let the receiver receive "uploaded Zipkin spans from a Java client application"
req, _ := http.NewRequest("POST", "https://tld.org/", strings.NewReader(zipkinSpansJSONJavaLibrary))
responseWriter := httptest.NewRecorder()
zi.ServeHTTP(responseWriter, req)
// Use the mock zipkin reporter to ensure all expected spans in a single batch. Since Flush waits for
// server response there is no need for further synchronization.
require.NoError(t, mzr.Flush())
// We expect back the exact JSON that was received
wants := []string{`
[{
"traceId": "4d1e00c0db9010db86154a4ba6e91385","parentId": "86154a4ba6e91385","id": "4d1e00c0db9010db",
"kind": "CLIENT","name": "get",
"timestamp": 1472470996199000,"duration": 207000,
"localEndpoint": {"serviceName": "frontend","ipv6": "7::80:807f"},
"remoteEndpoint": {"serviceName": "backend","ipv4": "192.168.99.101","port": 9000},
"annotations": [
{"timestamp": 1472470996238000,"value": "foo"},
{"timestamp": 1472470996403000,"value": "bar"}
],
"tags": {"http.path": "/api","clnt/finagle.version": "6.45.0"}
},
{
"traceId": "4d1e00c0db9010db86154a4ba6e91385","parentId": "86154a4ba6e91386","id": "4d1e00c0db9010dc",
"kind": "SERVER","name": "put",
"timestamp": 1472470996199000,"duration": 207000,
"localEndpoint": {"serviceName": "frontend","ipv6": "7::80:807f"},
"remoteEndpoint": {"serviceName": "frontend", "ipv4": "192.168.99.101","port": 9000},
"annotations": [
{"timestamp": 1472470996238000,"value": "foo"},
{"timestamp": 1472470996403000,"value": "bar"}
],
"tags": {"http.path": "/api","clnt/finagle.version": "6.45.0"}
},
{
"traceId": "4d1e00c0db9010db86154a4ba6e91385",
"parentId": "86154a4ba6e91386",
"id": "4d1e00c0db9010dd",
"kind": "SERVER",
"name": "put",
"timestamp": 1472470996199000,
"duration": 207000
}]
`}
for i, s := range wants {
want := unmarshalZipkinSpanArrayToMap(t, s)
gotBytes := buf.Next(int(sizes[i]))
got := unmarshalZipkinSpanArrayToMap(t, string(gotBytes))
for id, expected := range want {
actual, ok := got[id]
assert.True(t, ok)
assert.Equal(t, expected.ID, actual.ID)
assert.Equal(t, expected.Name, actual.Name)
assert.Equal(t, expected.TraceID, actual.TraceID)
assert.Equal(t, expected.Timestamp, actual.Timestamp)
assert.Equal(t, expected.Duration, actual.Duration)
assert.Equal(t, expected.Kind, actual.Kind)
}
}
}
type mockZipkinReporter struct {
url string
client *http.Client
batch []*zipkinmodel.SpanModel
serializer zipkinreporter.SpanSerializer
}
var _ zipkinreporter.Reporter = (*mockZipkinReporter)(nil)
func (r *mockZipkinReporter) Send(span zipkinmodel.SpanModel) {
r.batch = append(r.batch, &span)
}
func (r *mockZipkinReporter) Close() error {
return nil
}
func newMockZipkinReporter(url string) *mockZipkinReporter {
return &mockZipkinReporter{
url: url,
client: &http.Client{},
serializer: zipkinreporter.JSONSerializer{},
}
}
func (r *mockZipkinReporter) Flush() error {
sendBatch := r.batch
r.batch = nil
if len(sendBatch) == 0 {
return nil
}
body, err := r.serializer.Serialize(sendBatch)
if err != nil {
return err
}
req, err := http.NewRequest("POST", r.url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", r.serializer.ContentType())
resp, err := r.client.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("http request failed with status code %d", resp.StatusCode)
}
return nil
}
const zipkinSpansJSONJavaLibrary = `
[{
"traceId": "4d1e00c0db9010db86154a4ba6e91385",
"parentId": "86154a4ba6e91385",
"id": "4d1e00c0db9010db",
"kind": "CLIENT",
"name": "get",
"timestamp": 1472470996199000,
"duration": 207000,
"localEndpoint": {
"serviceName": "frontend",
"ipv6": "7::0.128.128.127"
},
"remoteEndpoint": {
"serviceName": "backend",
"ipv4": "192.168.99.101",
"port": 9000
},
"annotations": [
{
"timestamp": 1472470996238000,
"value": "foo"
},
{
"timestamp": 1472470996403000,
"value": "bar"
}
],
"tags": {
"http.path": "/api",
"clnt/finagle.version": "6.45.0"
}
},
{
"traceId": "4d1e00c0db9010db86154a4ba6e91385",
"parentId": "86154a4ba6e91386",
"id": "4d1e00c0db9010dc",
"kind": "SERVER",
"name": "put",
"timestamp": 1472470996199000,
"duration": 207000,
"localEndpoint": {
"serviceName": "frontend",
"ipv6": "7::0.128.128.127"
},
"remoteEndpoint": {
"serviceName": "frontend",
"ipv4": "192.168.99.101",
"port": 9000
},
"annotations": [
{
"timestamp": 1472470996238000,
"value": "foo"
},
{
"timestamp": 1472470996403000,
"value": "bar"
}
],
"tags": {
"http.path": "/api",
"clnt/finagle.version": "6.45.0"
}
},
{
"traceId": "4d1e00c0db9010db86154a4ba6e91385",
"parentId": "86154a4ba6e91386",
"id": "4d1e00c0db9010dd",
"kind": "SERVER",
"name": "put",
"timestamp": 1472470996199000,
"duration": 207000
}]
`
func TestZipkinExporter_invalidFormat(t *testing.T) {
config := &Config{
HTTPClientSettings: confighttp.HTTPClientSettings{
Endpoint: "1.2.3.4",
},
Format: "foobar",
}
f := NewFactory()
params := component.ExporterCreateParams{Logger: zap.NewNop()}
_, err := f.CreateTracesExporter(context.Background(), params, config)
require.Error(t, err)
}
// The rest of the fields should match up exactly
func TestZipkinExporter_roundtripProto(t *testing.T) {
buf := new(bytes.Buffer)
var contentType string
cst := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(buf, r.Body)
contentType = r.Header.Get("Content-Type")
r.Body.Close()
}))
defer cst.Close()
cfg := &Config{
HTTPClientSettings: confighttp.HTTPClientSettings{
Endpoint: cst.URL,
},
Format: "proto",
}
zexp, err := NewFactory().CreateTracesExporter(context.Background(), component.ExporterCreateParams{Logger: zap.NewNop()}, cfg)
require.NoError(t, err)
// The test requires the spans from zipkinSpansJSONJavaLibrary to be sent in a single batch, use
// a mock to ensure that this happens as intended.
mzr := newMockZipkinReporter(cst.URL)
mzr.serializer = zipkin_proto3.SpanSerializer{}
// Run the Zipkin receiver to "receive spans upload from a client application"
port := testutil.GetAvailablePort(t)
recvCfg := &zipkinreceiver.Config{
ReceiverSettings: config.ReceiverSettings{
NameVal: "zipkin_receiver",
},
HTTPServerSettings: confighttp.HTTPServerSettings{
Endpoint: fmt.Sprintf(":%d", port),
},
}
zi, err := zipkinreceiver.New(recvCfg, zexp)
require.NoError(t, err)
err = zi.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, err)
defer zi.Shutdown(context.Background())
// Let the receiver receive "uploaded Zipkin spans from a Java client application"
req, _ := http.NewRequest("POST", "https://tld.org/", strings.NewReader(zipkinSpansJSONJavaLibrary))
responseWriter := httptest.NewRecorder()
zi.ServeHTTP(responseWriter, req)
// Use the mock zipkin reporter to ensure all expected spans in a single batch. Since Flush waits for
// server response there is no need for further synchronization.
err = mzr.Flush()
require.NoError(t, err)
require.Equal(t, zipkin_proto3.SpanSerializer{}.ContentType(), contentType)
// Finally we need to inspect the output
gotBytes, err := ioutil.ReadAll(buf)
require.NoError(t, err)
_, err = zipkin_proto3.ParseSpans(gotBytes, false)
require.NoError(t, err)
}