1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,215 @@
# Zipkin Input Plugin
This plugin implements the Zipkin http server to gather trace and timing data
needed to troubleshoot latency problems in microservice architectures.
__Please Note:__ This plugin is experimental; Its data schema may be subject to
change based on its main usage cases and the evolution of the OpenTracing
standard.
> [!IMPORTANT]
> This plugin will create high cardinality data, so please take this into
> account when sending data to your output!
## Service Input <!-- @/docs/includes/service_input.md -->
This plugin is a service input. Normal plugins gather metrics determined by the
interval setting. Service plugins start a service to listen and wait for
metrics or events to occur. Service plugins have two key differences from
normal plugins:
1. The global or plugin specific `interval` setting may not apply
2. The CLI options of `--test`, `--test-wait`, and `--once` may not produce
output for this plugin
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Gather data from a Zipkin server including trace and timing data
[[inputs.zipkin]]
## URL path for span data
# path = "/api/v1/spans"
## Port on which Telegraf listens
# port = 9411
## Maximum duration before timing out read of the request
# read_timeout = "10s"
## Maximum duration before timing out write of the response
# write_timeout = "10s"
```
The plugin accepts spans in `JSON` or `thrift` if the `Content-Type` is
`application/json` or `application/x-thrift`, respectively. If `Content-Type`
is not set, then the plugin assumes it is `JSON` format.
## Tracing
This plugin uses Annotations tags and fields to track data from spans
- `TRACE` is a set of spans that share a single root span. Traces are built by
collecting all Spans that share a traceId.
- `SPAN` is a set of Annotations and BinaryAnnotations that correspond to a
particular RPC.
- `Annotations` create a metric for each annotation & binary annotation of a
span. This records an occurrence in time at the beginning and end of each
request.
Annotations may have the following values:
- `CS` (client start) marks the beginning of the span, a request is made.
- `SR` (server receive) marks the point in time the server receives the request
and starts processing it. Network latency & clock jitters distinguish this
from `CS`.
- `SS` (server send) marks the point in time the server is finished processing
and sends a request back to client. The difference to `SR` denotes the
amount of time it took to process the request.
- `CR` (client receive) marks the end of the span, with the client receiving
the response from server. RPC is considered complete with this annotation.
## Metrics
- `duration_ns` the time in nanoseconds between the end and beginning of a span
### Tags
- `id` the 64-bit ID of the span.
- `parent_id` an ID associated with a particular child span. If there is no
child span, `parent_id` is equal to `id`
- `trace_id` the 64-bit or 128-bit ID of a particular trace. Every span in a
trace uses this ID.
- `name` defines a span
#### Annotations have these additional tags
- `service_name` defines a service
- `annotation` the value of an annotation
- `endpoint_host` listening IPv4 address and, if present, port
#### Binary Annotations have these additional tag
- `service_name` defines a service
- `annotation` the value of an annotation
- `endpoint_host` listening IPv4 address and, if present, port
- `annotation_key` label describing the annotation
## Example Output
The Zipkin data
```json
[
{
"trace_id": 2505404965370368069,
"name": "Child",
"id": 8090652509916334619,
"parent_id": 22964302721410078,
"annotations": [],
"binary_annotations": [
{
"key": "lc",
"value": "dHJpdmlhbA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"timestamp": 1498688360851331,
"duration": 53106
},
{
"trace_id": 2505404965370368069,
"name": "Child",
"id": 103618986556047333,
"parent_id": 22964302721410078,
"annotations": [],
"binary_annotations": [
{
"key": "lc",
"value": "dHJpdmlhbA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"timestamp": 1498688360904552,
"duration": 50410
},
{
"trace_id": 2505404965370368069,
"name": "Parent",
"id": 22964302721410078,
"annotations": [
{
"timestamp": 1498688360851325,
"value": "Starting child #0",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
},
{
"timestamp": 1498688360904545,
"value": "Starting child #1",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
},
{
"timestamp": 1498688360954992,
"value": "A Log",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"binary_annotations": [
{
"key": "lc",
"value": "dHJpdmlhbA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"timestamp": 1498688360851318,
"duration": 103680
}
]
```
generated the following metrics
```text
zipkin,id=7047c59776af8a1b,name=child,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=53106000i 1498688360851331000
zipkin,annotation=trivial,annotation_key=lc,endpoint_host=127.0.0.1,id=7047c59776af8a1b,name=child,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=53106000i 1498688360851331000
zipkin,id=17020eb55a8bfe5,name=child,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=50410000i 1498688360904552000
zipkin,annotation=trivial,annotation_key=lc,endpoint_host=127.0.0.1,id=17020eb55a8bfe5,name=child,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=50410000i 1498688360904552000
zipkin,id=5195e96239641e,name=parent,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=103680000i 1498688360851318000
zipkin,annotation=Starting\ child\ #0,endpoint_host=127.0.0.1,id=5195e96239641e,name=parent,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=103680000i 1498688360851318000
zipkin,annotation=Starting\ child\ #1,endpoint_host=127.0.0.1,id=5195e96239641e,name=parent,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=103680000i 1498688360851318000
zipkin,annotation=A\ Log,endpoint_host=127.0.0.1,id=5195e96239641e,name=parent,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=103680000i 1498688360851318000
zipkin,annotation=trivial,annotation_key=lc,endpoint_host=127.0.0.1,id=5195e96239641e,name=parent,parent_id=5195e96239641e,service_name=trivial,trace_id=22c4fc8ab3669045 duration_ns=103680000i 1498688360851318000
```

View file

@ -0,0 +1,81 @@
/*
This is a development testing cli tool meant to stress the zipkin telegraf plugin.
It writes a specified number of zipkin spans to the plugin endpoint, with other
parameters which dictate batch size and flush timeout.
Usage as follows:
`./stress_test_write -batch_size=<batch_size> -max_backlog=<max_span_buffer_backlog> -batch_interval=<batch_interval_in_seconds> \
-span_count<number_of_spans_to_write> -zipkin_host=<zipkin_service_hostname>`
Or with a timer:
`time ./stress_test_write -batch_size=<batch_size> -max_backlog=<max_span_buffer_backlog> -batch_interval=<batch_interval_in_seconds> \
-span_count<number_of_spans_to_write> -zipkin_host=<zipkin_service_hostname>`
However, the flag defaults work just fine for a good write stress test (and are what
this tool has mainly been tested with), so there shouldn't be much need to
manually tweak the parameters.
*/
package main
import (
"flag"
"fmt"
"log"
"time"
otlog "github.com/opentracing/opentracing-go/log"
zipkinot "github.com/openzipkin-contrib/zipkin-go-opentracing"
"github.com/openzipkin/zipkin-go"
zipkinhttp "github.com/openzipkin/zipkin-go/reporter/http"
)
var (
batchSize int
maxBackLog int
batchTimeInterval int
spanCount int
zipkinServerHost string
)
func init() {
flag.IntVar(&batchSize, "batch_size", 10000, "")
flag.IntVar(&maxBackLog, "max_backlog", 100000, "")
flag.IntVar(&batchTimeInterval, "batch_interval", 1, "")
flag.IntVar(&spanCount, "span_count", 100000, "")
flag.StringVar(&zipkinServerHost, "zipkin_host", "localhost", "")
}
func main() {
flag.Parse()
var hostname = fmt.Sprintf("http://%s:9411/api/v1/spans", zipkinServerHost)
reporter := zipkinhttp.NewReporter(
hostname,
zipkinhttp.BatchSize(batchSize),
zipkinhttp.MaxBacklog(maxBackLog),
zipkinhttp.BatchInterval(time.Duration(batchTimeInterval)*time.Second),
)
defer reporter.Close()
endpoint, err := zipkin.NewEndpoint("Trivial", "127.0.0.1:0")
if err != nil {
log.Panicf("Error: %v\n", err)
}
nativeTracer, err := zipkin.NewTracer(reporter, zipkin.WithLocalEndpoint(endpoint))
if err != nil {
log.Panicf("Error: %v\n", err)
}
tracer := zipkinot.Wrap(nativeTracer)
log.Printf("Writing %d spans to zipkin server at %s\n", spanCount, hostname)
for i := 0; i < spanCount; i++ {
parent := tracer.StartSpan("Parent")
parent.LogFields(otlog.Message(fmt.Sprintf("Trace%d", i)))
parent.Finish()
}
log.Println("Done. Flushing remaining spans...")
}

View file

@ -0,0 +1,148 @@
/*
A small cli utility meant to convert json to zipkin thrift binary format, and
vice versa.
To convert from json to thrift,
the json is unmarshalled, converted to zipkincore.Span structures, and
marshalled into thrift binary protocol. The json must be in an array format (even if it only has one object),
because the tool automatically tries to unmarshal the json into an array of structs.
To convert from thrift to json,
the opposite process must happen. The thrift binary data must be read into an array of
zipkin span structures, and those spans must be marshalled into json.
Usage:
./thrift_serialize -input <input-file> -output <output-file> -deserialize<true|false>
If `deserialize` is set to true (false by default), the tool will interpret the input file as
thrift, and write it as json to the output file.
Otherwise, the input file will be interpreted as json, and the output will be encoded as thrift.
*/
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"os"
"github.com/apache/thrift/lib/go/thrift"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec/thrift/gen-go/zipkincore"
)
var (
filename string
outFileName string
inputType string
)
const usage = `./json_serialize -input <input> -output output -input-type<json|thrift>`
func init() {
flag.StringVar(&filename, "input", "", usage)
flag.StringVar(&outFileName, "output", "", usage)
flag.StringVar(&inputType, "input-type", "thrift", usage)
}
func main() {
flag.Parse()
contents, err := os.ReadFile(filename)
if err != nil {
log.Fatalf("Error reading file: %v\n", err)
}
switch inputType {
case "json":
raw, err := jsonToZipkinThrift(contents)
if err != nil {
log.Fatalf("%v\n", err)
}
if err := os.WriteFile(outFileName, raw, 0640); err != nil {
log.Fatalf("%v", err)
}
case "thrift":
raw, err := thriftToJSONSpans(contents)
if err != nil {
log.Fatalf("%v\n", err)
}
if err := os.WriteFile(outFileName, raw, 0640); err != nil {
log.Fatalf("%v", err)
}
default:
log.Fatalf("Unsupported input type")
}
}
func jsonToZipkinThrift(jsonRaw []byte) ([]byte, error) {
if len(jsonRaw) == 0 {
return nil, errors.New("no data")
}
if string(jsonRaw)[0] != '[' {
return nil, errors.New("cannot unmarshal non array type")
}
var spans []*zipkincore.Span
err := json.Unmarshal(jsonRaw, &spans)
if err != nil {
return nil, fmt.Errorf("error unmarshalling: %w", err)
}
var zspans []*zipkincore.Span
if err != nil {
return nil, err
}
zspans = append(zspans, spans...)
buf := thrift.NewTMemoryBuffer()
transport := thrift.NewTBinaryProtocolConf(buf, nil)
if err = transport.WriteListBegin(context.Background(), thrift.STRUCT, len(spans)); err != nil {
return nil, fmt.Errorf("error in beginning thrift write: %w", err)
}
for _, span := range zspans {
err = span.Write(context.Background(), transport)
if err != nil {
return nil, fmt.Errorf("error converting zipkin struct to thrift: %w", err)
}
}
if err = transport.WriteListEnd(context.Background()); err != nil {
return nil, fmt.Errorf("error finishing thrift write: %w", err)
}
return buf.Bytes(), nil
}
func thriftToJSONSpans(thriftData []byte) ([]byte, error) {
buffer := thrift.NewTMemoryBuffer()
buffer.Write(thriftData)
transport := thrift.NewTBinaryProtocolConf(buffer, nil)
_, size, err := transport.ReadListBegin(context.Background())
if err != nil {
return nil, fmt.Errorf("error in ReadListBegin: %w", err)
}
spans := make([]*zipkincore.Span, 0, size)
for i := 0; i < size; i++ {
zs := &zipkincore.Span{}
if err = zs.Read(context.Background(), transport); err != nil {
return nil, fmt.Errorf("error reading into zipkin struct: %w", err)
}
spans = append(spans, zs)
}
err = transport.ReadListEnd(context.Background())
if err != nil {
return nil, fmt.Errorf("error ending thrift read: %w", err)
}
return json.MarshalIndent(spans, "", " ")
}

View file

@ -0,0 +1,227 @@
package codec
import (
"time"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec/thrift/gen-go/zipkincore"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/trace"
)
// now is a mockable time for now
var now = time.Now
// DefaultServiceName when the span does not have any serviceName
const DefaultServiceName = "unknown"
// Decoder decodes the bytes and returns a trace
type Decoder interface {
// Decode decodes the given byte slice into a slice of Spans.
Decode(octets []byte) ([]Span, error)
}
// Span represents a span created by instrumentation in RPC clients or servers.
type Span interface {
// Trace returns the trace ID of the span.
Trace() (string, error)
// SpanID returns the span ID.
SpanID() (string, error)
// Parent returns the parent span ID.
Parent() (string, error)
// Name returns the name of the span.
Name() string
// Annotations returns the annotations of the span.
Annotations() []Annotation
// BinaryAnnotations returns the binary annotations of the span.
BinaryAnnotations() ([]BinaryAnnotation, error)
// Timestamp returns the timestamp of the span.
Timestamp() time.Time
// Duration returns the duration of the span.
Duration() time.Duration
}
// Annotation represents an event that explains latency with a timestamp.
type Annotation interface {
// Timestamp returns the timestamp of the annotation.
Timestamp() time.Time
// Value returns the value of the annotation.
Value() string
// Host returns the endpoint associated with the annotation.
Host() Endpoint
}
// BinaryAnnotation represents tags applied to a Span to give it context.
type BinaryAnnotation interface {
// Key returns the key of the binary annotation.
Key() string
// Value returns the value of the binary annotation.
Value() string
// Host returns the endpoint associated with the binary annotation.
Host() Endpoint
}
// Endpoint represents the network context of a service recording an annotation.
type Endpoint interface {
// Host returns the host address of the endpoint.
Host() string
// Name returns the name of the service associated with the endpoint.
Name() string
}
// defaultEndpoint is used if the annotations have no endpoints.
type defaultEndpoint struct{}
// Host returns 0.0.0.0; used when the host is unknown.
func (*defaultEndpoint) Host() string { return "0.0.0.0" }
// Name returns "unknown" when an endpoint doesn't exist.
func (*defaultEndpoint) Name() string { return DefaultServiceName }
// MicroToTime converts zipkin's native time of microseconds into time.Time.
func MicroToTime(micro int64) time.Time {
return time.Unix(0, micro*int64(time.Microsecond)).UTC()
}
// NewTrace converts a slice of Spans into a new Trace.
func NewTrace(spans []Span) (trace.Trace, error) {
tr := make(trace.Trace, len(spans))
for i, span := range spans {
bin, err := span.BinaryAnnotations()
if err != nil {
return nil, err
}
endpoint := serviceEndpoint(span.Annotations(), bin)
id, err := span.SpanID()
if err != nil {
return nil, err
}
tid, err := span.Trace()
if err != nil {
return nil, err
}
pid, err := parentID(span)
if err != nil {
return nil, err
}
tr[i] = trace.Span{
ID: id,
TraceID: tid,
Name: span.Name(),
Timestamp: guessTimestamp(span),
Duration: convertDuration(span),
ParentID: pid,
ServiceName: endpoint.Name(),
Annotations: NewAnnotations(span.Annotations(), endpoint),
BinaryAnnotations: NewBinaryAnnotations(bin, endpoint),
}
}
return tr, nil
}
// NewAnnotations converts a slice of Annotation into a slice of new Annotations
func NewAnnotations(annotations []Annotation, endpoint Endpoint) []trace.Annotation {
formatted := make([]trace.Annotation, 0, len(annotations))
for _, annotation := range annotations {
formatted = append(formatted, trace.Annotation{
Host: endpoint.Host(),
ServiceName: endpoint.Name(),
Timestamp: annotation.Timestamp(),
Value: annotation.Value(),
})
}
return formatted
}
// NewBinaryAnnotations is very similar to NewAnnotations, but it
// converts BinaryAnnotations instead of the normal Annotation
func NewBinaryAnnotations(annotations []BinaryAnnotation, endpoint Endpoint) []trace.BinaryAnnotation {
formatted := make([]trace.BinaryAnnotation, 0, len(annotations))
for _, annotation := range annotations {
formatted = append(formatted, trace.BinaryAnnotation{
Host: endpoint.Host(),
ServiceName: endpoint.Name(),
Key: annotation.Key(),
Value: annotation.Value(),
})
}
return formatted
}
func minMax(span Span) (low, high time.Time) {
low = now().UTC()
high = time.Time{}.UTC()
for _, annotation := range span.Annotations() {
ts := annotation.Timestamp()
if !ts.IsZero() && ts.Before(low) {
low = ts
}
if !ts.IsZero() && ts.After(high) {
high = ts
}
}
if high.IsZero() {
high = low
}
return low, high
}
func guessTimestamp(span Span) time.Time {
ts := span.Timestamp()
if !ts.IsZero() {
return ts
}
low, _ := minMax(span)
return low
}
func convertDuration(span Span) time.Duration {
duration := span.Duration()
if duration != 0 {
return duration
}
low, high := minMax(span)
return high.Sub(low)
}
func parentID(span Span) (string, error) {
// A parent ID of "" means that this is a parent span. In this case,
// we set the parent ID of the span to be its own id, so it points to
// itself.
id, err := span.Parent()
if err != nil {
return "", err
}
if id != "" {
return id, nil
}
return span.SpanID()
}
func serviceEndpoint(ann []Annotation, bann []BinaryAnnotation) Endpoint {
for _, a := range ann {
switch a.Value() {
case zipkincore.SERVER_RECV, zipkincore.SERVER_SEND, zipkincore.CLIENT_RECV, zipkincore.CLIENT_SEND:
if a.Host() != nil && a.Host().Name() != "" {
return a.Host()
}
}
}
for _, a := range bann {
if a.Key() == zipkincore.LOCAL_COMPONENT && a.Host() != nil && a.Host().Name() != "" {
return a.Host()
}
}
// Unable to find any "standard" endpoint host, so, use any that exist in the regular annotations
for _, a := range ann {
if a.Host() != nil && a.Host().Name() != "" {
return a.Host()
}
}
return &defaultEndpoint{}
}

View file

@ -0,0 +1,613 @@
package codec
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/trace"
)
func Test_MicroToTime(t *testing.T) {
tests := []struct {
name string
micro int64
want time.Time
}{
{
name: "given zero micro seconds expected unix time zero",
micro: 0,
want: time.Unix(0, 0).UTC(),
},
{
name: "given a million micro seconds expected unix time one",
micro: 1000000,
want: time.Unix(1, 0).UTC(),
},
{
name: "given a million micro seconds expected unix time one",
micro: 1503031538791000,
want: time.Unix(0, 1503031538791000000).UTC(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, MicroToTime(tt.micro))
})
}
}
func Test_minMax(t *testing.T) {
tests := []struct {
name string
span *MockSpan
now func() time.Time
wantMin time.Time
wantMax time.Time
}{
{
name: "Single annotation",
span: &MockSpan{
Anno: []Annotation{
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(time.Second),
},
},
},
wantMin: time.Unix(1, 0).UTC(),
wantMax: time.Unix(1, 0).UTC(),
},
{
name: "Three annotations",
span: &MockSpan{
Anno: []Annotation{
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(1 * time.Second),
},
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(2 * time.Second),
},
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(3 * time.Second),
},
},
},
wantMin: time.Unix(1, 0).UTC(),
wantMax: time.Unix(3, 0).UTC(),
},
{
name: "Annotations are in the future",
span: &MockSpan{
Anno: []Annotation{
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(3 * time.Second),
},
},
},
wantMin: time.Unix(2, 0).UTC(),
wantMax: time.Unix(3, 0).UTC(),
now: func() time.Time {
return time.Unix(2, 0).UTC()
},
},
{
name: "No Annotations",
span: &MockSpan{},
wantMin: time.Unix(2, 0).UTC(),
wantMax: time.Unix(2, 0).UTC(),
now: func() time.Time {
return time.Unix(2, 0).UTC()
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.now != nil {
now = tt.now
}
gotMin, gotMax := minMax(tt.span)
require.Equal(t, tt.wantMin, gotMin)
require.Equal(t, tt.wantMax, gotMax)
now = time.Now
})
}
}
func Test_guessTimestamp(t *testing.T) {
tests := []struct {
name string
span Span
now func() time.Time
want time.Time
}{
{
name: "simple timestamp",
span: &MockSpan{
Time: time.Unix(2, 0).UTC(),
},
want: time.Unix(2, 0).UTC(),
},
{
name: "zero timestamp",
span: &MockSpan{
Time: time.Time{},
},
now: func() time.Time {
return time.Unix(2, 0).UTC()
},
want: time.Unix(2, 0).UTC(),
},
{
name: "zero timestamp with single annotation",
span: &MockSpan{
Time: time.Time{},
Anno: []Annotation{
&MockAnnotation{
Time: time.Unix(0, 0).UTC(),
},
},
},
want: time.Unix(0, 0).UTC(),
},
{
name: "zero timestamp with two annotations",
span: &MockSpan{
Time: time.Time{},
Anno: []Annotation{
&MockAnnotation{
Time: time.Unix(0, 0).UTC(),
},
&MockAnnotation{
Time: time.Unix(2, 0).UTC(),
},
},
},
want: time.Unix(0, 0).UTC(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.now != nil {
now = tt.now
}
require.Equal(t, tt.want, guessTimestamp(tt.span))
now = time.Now
})
}
}
func Test_convertDuration(t *testing.T) {
tests := []struct {
name string
span Span
want time.Duration
}{
{
name: "simple duration",
span: &MockSpan{
Dur: time.Hour,
},
want: time.Hour,
},
{
name: "no timestamp, but, 2 seconds between annotations",
span: &MockSpan{
Anno: []Annotation{
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(1 * time.Second),
},
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(2 * time.Second),
},
&MockAnnotation{
Time: time.Unix(0, 0).UTC().Add(3 * time.Second),
},
},
},
want: 2 * time.Second,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, convertDuration(tt.span))
})
}
}
func Test_parentID(t *testing.T) {
tests := []struct {
name string
span Span
want string
wantErr bool
}{
{
name: "has parent id",
span: &MockSpan{
ParentID: "6b221d5bc9e6496c",
},
want: "6b221d5bc9e6496c",
},
{
name: "no parent, so use id",
span: &MockSpan{
ID: "abceasyas123",
},
want: "abceasyas123",
},
{
name: "bad parent value",
span: &MockSpan{
Error: errors.New("mommie dearest"),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parentID(tt.span)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}
func Test_serviceEndpoint(t *testing.T) {
tests := []struct {
name string
ann []Annotation
bann []BinaryAnnotation
want Endpoint
}{
{
name: "Annotation with server receive",
ann: []Annotation{
&MockAnnotation{
Val: "battery",
H: &MockEndpoint{
name: "aa",
},
},
&MockAnnotation{
Val: "sr",
H: &MockEndpoint{
name: "me",
},
},
},
want: &MockEndpoint{
name: "me",
},
},
{
name: "Annotation with no standard values",
ann: []Annotation{
&MockAnnotation{
Val: "noop",
},
&MockAnnotation{
Val: "aa",
H: &MockEndpoint{
name: "battery",
},
},
},
want: &MockEndpoint{
name: "battery",
},
},
{
name: "Annotation with no endpoints",
ann: []Annotation{
&MockAnnotation{
Val: "noop",
},
},
want: &defaultEndpoint{},
},
{
name: "Binary annotation with local component",
bann: []BinaryAnnotation{
&MockBinaryAnnotation{
K: "noop",
H: &MockEndpoint{
name: "aa",
},
},
&MockBinaryAnnotation{
K: "lc",
H: &MockEndpoint{
name: "me",
},
},
},
want: &MockEndpoint{
name: "me",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, serviceEndpoint(tt.ann, tt.bann))
})
}
}
func TestNewBinaryAnnotations(t *testing.T) {
tests := []struct {
name string
annotations []BinaryAnnotation
endpoint Endpoint
want []trace.BinaryAnnotation
}{
{
name: "Should override annotation with endpoint",
annotations: []BinaryAnnotation{
&MockBinaryAnnotation{
K: "mykey",
V: "myvalue",
H: &MockEndpoint{
host: "noop",
name: "noop",
},
},
},
endpoint: &MockEndpoint{
host: "myhost",
name: "myservice",
},
want: []trace.BinaryAnnotation{
{
Host: "myhost",
ServiceName: "myservice",
Key: "mykey",
Value: "myvalue",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, NewBinaryAnnotations(tt.annotations, tt.endpoint))
})
}
}
func TestNewAnnotations(t *testing.T) {
tests := []struct {
name string
annotations []Annotation
endpoint Endpoint
want []trace.Annotation
}{
{
name: "Should override annotation with endpoint",
annotations: []Annotation{
&MockAnnotation{
Time: time.Unix(0, 0).UTC(),
Val: "myvalue",
H: &MockEndpoint{
host: "noop",
name: "noop",
},
},
},
endpoint: &MockEndpoint{
host: "myhost",
name: "myservice",
},
want: []trace.Annotation{
{
Host: "myhost",
ServiceName: "myservice",
Timestamp: time.Unix(0, 0).UTC(),
Value: "myvalue",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, NewAnnotations(tt.annotations, tt.endpoint))
})
}
}
func TestNewTrace(t *testing.T) {
tests := []struct {
name string
spans []Span
now func() time.Time
want trace.Trace
wantErr bool
}{
{
name: "empty span",
spans: []Span{
&MockSpan{},
},
now: func() time.Time {
return time.Unix(0, 0).UTC()
},
want: trace.Trace{
trace.Span{
ServiceName: "unknown",
Timestamp: time.Unix(0, 0).UTC(),
Annotations: make([]trace.Annotation, 0),
BinaryAnnotations: make([]trace.BinaryAnnotation, 0),
},
},
},
{
name: "span has no id",
spans: []Span{
&MockSpan{
Error: errors.New("span has no id"),
},
},
wantErr: true,
},
{
name: "complete span",
spans: []Span{
&MockSpan{
TraceID: "tid",
ID: "id",
ParentID: "",
ServiceName: "me",
Anno: []Annotation{
&MockAnnotation{
Time: time.Unix(1, 0).UTC(),
Val: "myval",
H: &MockEndpoint{
host: "myhost",
name: "myname",
},
},
},
Time: time.Unix(0, 0).UTC(),
Dur: 2 * time.Second,
},
},
now: func() time.Time {
return time.Unix(0, 0).UTC()
},
want: trace.Trace{
trace.Span{
ID: "id",
ParentID: "id",
TraceID: "tid",
Name: "me",
ServiceName: "myname",
Timestamp: time.Unix(0, 0).UTC(),
Duration: 2 * time.Second,
Annotations: []trace.Annotation{
{
Timestamp: time.Unix(1, 0).UTC(),
Value: "myval",
Host: "myhost",
ServiceName: "myname",
},
},
BinaryAnnotations: make([]trace.BinaryAnnotation, 0),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.now != nil {
now = tt.now
}
got, err := NewTrace(tt.spans)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
now = time.Now
})
}
}
type MockSpan struct {
TraceID string
ID string
ParentID string
ServiceName string
Anno []Annotation
BinAnno []BinaryAnnotation
Time time.Time
Dur time.Duration
Error error
}
func (m *MockSpan) Trace() (string, error) {
return m.TraceID, m.Error
}
func (m *MockSpan) SpanID() (string, error) {
return m.ID, m.Error
}
func (m *MockSpan) Parent() (string, error) {
return m.ParentID, m.Error
}
func (m *MockSpan) Name() string {
return m.ServiceName
}
func (m *MockSpan) Annotations() []Annotation {
return m.Anno
}
func (m *MockSpan) BinaryAnnotations() ([]BinaryAnnotation, error) {
return m.BinAnno, m.Error
}
func (m *MockSpan) Timestamp() time.Time {
return m.Time
}
func (m *MockSpan) Duration() time.Duration {
return m.Dur
}
type MockAnnotation struct {
Time time.Time
Val string
H Endpoint
}
func (m *MockAnnotation) Timestamp() time.Time {
return m.Time
}
func (m *MockAnnotation) Value() string {
return m.Val
}
func (m *MockAnnotation) Host() Endpoint {
return m.H
}
type MockEndpoint struct {
host string
name string
}
func (e *MockEndpoint) Host() string {
return e.host
}
func (e *MockEndpoint) Name() string {
return e.name
}
type MockBinaryAnnotation struct {
Time time.Time
K string
V string
H Endpoint
}
func (b *MockBinaryAnnotation) Key() string {
return b.K
}
func (b *MockBinaryAnnotation) Value() string {
return b.V
}
func (b *MockBinaryAnnotation) Host() Endpoint {
return b.H
}

View file

@ -0,0 +1,271 @@
package json_v1
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec/thrift/gen-go/zipkincore"
)
// JSON decodes spans from bodies `POST`ed to the spans endpoint
type JSON struct{}
// Decode unmarshals and validates the JSON body
func (*JSON) Decode(octets []byte) ([]codec.Span, error) {
var spans []span
err := json.Unmarshal(octets, &spans)
if err != nil {
return nil, err
}
res := make([]codec.Span, 0, len(spans))
for i := range spans {
if err := spans[i].validate(); err != nil {
return nil, err
}
res = append(res, &spans[i])
}
return res, nil
}
type span struct {
TraceID string `json:"traceId"`
SpanName string `json:"name"`
ParentID string `json:"parentId,omitempty"`
ID string `json:"id"`
Time *int64 `json:"timestamp,omitempty"`
Dur *int64 `json:"duration,omitempty"`
Debug bool `json:"debug,omitempty"`
Anno []annotation `json:"annotations"`
BAnno []binaryAnnotation `json:"binaryAnnotations"`
}
func (s *span) validate() error {
var err error
check := func(f func() (string, error)) {
if err != nil {
return
}
_, err = f()
}
check(s.Trace)
check(s.SpanID)
check(s.Parent)
if err != nil {
return err
}
_, err = s.BinaryAnnotations()
return err
}
// Trace returns the trace ID of the span and an error if the trace ID is empty.
func (s *span) Trace() (string, error) {
if s.TraceID == "" {
return "", errors.New("trace ID cannot be null")
}
return traceIDFromString(s.TraceID)
}
// SpanID returns the span ID of the span and returns an error if the span ID is empty.
func (s *span) SpanID() (string, error) {
if s.ID == "" {
return "", errors.New("span ID cannot be null")
}
return idFromString(s.ID)
}
// Parent returns the parent span ID of the span.
func (s *span) Parent() (string, error) {
if s.ParentID == "" {
return "", nil
}
return idFromString(s.ParentID)
}
// Name returns the name of the span.
func (s *span) Name() string {
return s.SpanName
}
// Annotations returns the annotations of the span as a slice of codec.Annotation.
func (s *span) Annotations() []codec.Annotation {
res := make([]codec.Annotation, 0, len(s.Anno))
for i := range s.Anno {
res = append(res, &s.Anno[i])
}
return res
}
// BinaryAnnotations returns the binary annotations of the span as a slice of codec.BinaryAnnotation.
func (s *span) BinaryAnnotations() ([]codec.BinaryAnnotation, error) {
res := make([]codec.BinaryAnnotation, 0, len(s.BAnno))
for i, a := range s.BAnno {
if a.Key() != "" && a.Value() == "" {
return nil, fmt.Errorf("no value for key %s at binaryAnnotations[%d]", a.K, i)
}
if a.Value() != "" && a.Key() == "" {
return nil, fmt.Errorf("no key at binaryAnnotations[%d]", i)
}
res = append(res, &s.BAnno[i])
}
return res, nil
}
// Timestamp returns the timestamp of the span as a time.Time object.
// It returns a zero time if the timestamp is not set.
func (s *span) Timestamp() time.Time {
if s.Time == nil {
return time.Time{}
}
return codec.MicroToTime(*s.Time)
}
// Duration returns the duration of the span as a time.Duration object.
// It returns zero if the duration is not set.
func (s *span) Duration() time.Duration {
if s.Dur == nil {
return 0
}
return time.Duration(*s.Dur) * time.Microsecond
}
type annotation struct {
Endpoint *endpoint `json:"endpoint,omitempty"`
Time int64 `json:"timestamp"`
Val string `json:"value,omitempty"`
}
// Timestamp returns the timestamp of the annotation as a time.Time object.
func (a *annotation) Timestamp() time.Time {
return codec.MicroToTime(a.Time)
}
// Value returns the value of the annotation as a string.
func (a *annotation) Value() string {
return a.Val
}
// Host returns the endpoint associated with the annotation as a codec.Endpoint.
func (a *annotation) Host() codec.Endpoint {
return a.Endpoint
}
type binaryAnnotation struct {
K string `json:"key"`
V json.RawMessage `json:"value"`
Type string `json:"type"`
Endpoint *endpoint `json:"endpoint,omitempty"`
}
// Key returns the key of the binary annotation as a string.
func (b *binaryAnnotation) Key() string {
return b.K
}
// Value returns the value of the binary annotation as a string.
func (b *binaryAnnotation) Value() string {
t, err := zipkincore.AnnotationTypeFromString(b.Type)
// Assume this is a string if we cannot tell the type
if err != nil {
t = zipkincore.AnnotationType_STRING
}
switch t {
case zipkincore.AnnotationType_BOOL:
var v bool
err := json.Unmarshal(b.V, &v)
if err == nil {
return strconv.FormatBool(v)
}
case zipkincore.AnnotationType_BYTES:
return string(b.V)
case zipkincore.AnnotationType_I16, zipkincore.AnnotationType_I32, zipkincore.AnnotationType_I64:
var v int64
err := json.Unmarshal(b.V, &v)
if err == nil {
return strconv.FormatInt(v, 10)
}
case zipkincore.AnnotationType_DOUBLE:
var v float64
err := json.Unmarshal(b.V, &v)
if err == nil {
return strconv.FormatFloat(v, 'f', -1, 64)
}
case zipkincore.AnnotationType_STRING:
var v string
err := json.Unmarshal(b.V, &v)
if err == nil {
return v
}
}
return ""
}
// Host returns the endpoint associated with the binary annotation as a codec.Endpoint.
func (b *binaryAnnotation) Host() codec.Endpoint {
return b.Endpoint
}
type endpoint struct {
ServiceName string `json:"serviceName"`
Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6,omitempty"`
Port int `json:"port"`
}
// Host returns the host of the endpoint as a string.
func (e *endpoint) Host() string {
if e.Port != 0 {
return fmt.Sprintf("%s:%d", e.Ipv4, e.Port)
}
return e.Ipv4
}
// Name returns the service name of the endpoint.
func (e *endpoint) Name() string {
return e.ServiceName
}
// traceIDFromString creates a TraceID from a hexadecimal string
func traceIDFromString(s string) (string, error) {
var hi, lo uint64
var err error
if len(s) > 32 {
return "", fmt.Errorf("length of TraceID cannot be greater than 32 hex characters: %s", s)
} else if len(s) > 16 {
hiLen := len(s) - 16
if hi, err = strconv.ParseUint(s[0:hiLen], 16, 64); err != nil {
return "", err
}
if lo, err = strconv.ParseUint(s[hiLen:], 16, 64); err != nil {
return "", err
}
} else {
if lo, err = strconv.ParseUint(s, 16, 64); err != nil {
return "", err
}
}
if hi == 0 {
return strconv.FormatUint(lo, 16), nil
}
return fmt.Sprintf("%x%016x", hi, lo), nil
}
// idFromString validates the ID and returns it in hexadecimal format.
func idFromString(s string) (string, error) {
if len(s) > 16 {
return "", fmt.Errorf("length of ID cannot be greater than 16 hex characters: %s", s)
}
id, err := strconv.ParseUint(s, 16, 64)
if err != nil {
return "", err
}
return strconv.FormatUint(id, 16), nil
}

View file

@ -0,0 +1,894 @@
package json_v1
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec"
)
func TestJSON_Decode(t *testing.T) {
addr := func(i int64) *int64 { return &i }
tests := []struct {
name string
octets []byte
want []codec.Span
wantErr bool
}{
{
name: "bad json is error",
octets: []byte(`
[
{
]`),
wantErr: true,
},
{
name: "Decodes simple trace",
octets: []byte(`
[
{
"traceId": "6b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c"
}
]`),
want: []codec.Span{
&span{
TraceID: "6b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
},
},
},
{
name: "Decodes two spans",
octets: []byte(`
[
{
"traceId": "6b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c"
},
{
"traceId": "6b221d5bc9e6496c",
"name": "get-traces",
"id": "c6946e9cb5d122b6",
"parentId": "6b221d5bc9e6496c",
"duration": 10000
}
]`),
want: []codec.Span{
&span{
TraceID: "6b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
},
&span{
TraceID: "6b221d5bc9e6496c",
SpanName: "get-traces",
ID: "c6946e9cb5d122b6",
ParentID: "6b221d5bc9e6496c",
Dur: addr(10000),
},
},
},
{
name: "Decodes trace with timestamp",
octets: []byte(`
[
{
"traceId": "6b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"timestamp": 1503031538791000
}
]`),
want: []codec.Span{
&span{
TraceID: "6b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
Time: addr(1503031538791000),
},
},
},
{
name: "Decodes simple trace with high and low trace id",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c"
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
},
},
},
{
name: "Error when trace id is null",
octets: []byte(`
[
{
"traceId": null,
"name": "get-traces",
"id": "6b221d5bc9e6496c"
}
]`),
wantErr: true,
},
{
name: "ignore null parentId",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"parentId": null
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
},
},
},
{
name: "ignore null timestamp",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"timestamp": null
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
},
},
},
{
name: "ignore null duration",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"duration": null
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
},
},
},
{
name: "ignore null annotation endpoint",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"annotations": [
{
"timestamp": 1461750491274000,
"value": "cs",
"endpoint": null
}
]
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
Anno: []annotation{
{
Time: 1461750491274000,
Val: "cs",
},
},
},
},
},
{
name: "ignore null binary annotation endpoint",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"key": "lc",
"value": "JDBCSpanStore",
"endpoint": null
}
]
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
BAnno: []binaryAnnotation{
{
K: "lc",
V: json.RawMessage(`"JDBCSpanStore"`),
},
},
},
},
},
{
name: "Error when binary annotation has no key",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"value": "JDBCSpanStore",
"endpoint": null
}
]
}
]`),
wantErr: true,
},
{
name: "Error when binary annotation has no value",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"key": "lc",
"endpoint": null
}
]
}
]`),
wantErr: true,
},
{
name: "binary annotation with endpoint",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"key": "lc",
"value": "JDBCSpanStore",
"endpoint": {
"serviceName": "service",
"port": 65535
}
}
]
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
BAnno: []binaryAnnotation{
{
K: "lc",
V: json.RawMessage(`"JDBCSpanStore"`),
Endpoint: &endpoint{
ServiceName: "service",
Port: 65535,
},
},
},
},
},
},
{
name: "binary annotation with double value",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"key": "num",
"value": 1.23456789,
"type": "DOUBLE"
}
]
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
BAnno: []binaryAnnotation{
{
K: "num",
V: json.RawMessage{0x31, 0x2e, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39},
Type: "DOUBLE",
},
},
},
},
},
{
name: "binary annotation with integer value",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"key": "num",
"value": 1,
"type": "I16"
}
]
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
BAnno: []binaryAnnotation{
{
K: "num",
V: json.RawMessage{0x31},
Type: "I16",
},
},
},
},
},
{
name: "binary annotation with bool value",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"key": "num",
"value": true,
"type": "BOOL"
}
]
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
BAnno: []binaryAnnotation{
{
K: "num",
V: json.RawMessage(`true`),
Type: "BOOL",
},
},
},
},
},
{
name: "binary annotation with bytes value",
octets: []byte(`
[
{
"traceId": "48485a3953bb61246b221d5bc9e6496c",
"name": "get-traces",
"id": "6b221d5bc9e6496c",
"binaryAnnotations": [
{
"key": "num",
"value": "1",
"type": "BYTES"
}
]
}
]`),
want: []codec.Span{
&span{
TraceID: "48485a3953bb61246b221d5bc9e6496c",
SpanName: "get-traces",
ID: "6b221d5bc9e6496c",
BAnno: []binaryAnnotation{
{
K: "num",
V: json.RawMessage(`"1"`),
Type: "BYTES",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
j := &JSON{}
got, err := j.Decode(tt.octets)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}
func Test_span_Trace(t *testing.T) {
tests := []struct {
name string
TraceID string
want string
wantErr bool
}{
{
name: "Trace IDs cannot be null",
TraceID: "",
wantErr: true,
},
{
name: "converts hex string correctly",
TraceID: "deadbeef",
want: "deadbeef",
},
{
name: "converts high and low trace id correctly",
TraceID: "48485a3953bb61246b221d5bc9e6496c",
want: "48485a3953bb61246b221d5bc9e6496c",
},
{
name: "errors when string isn't hex",
TraceID: "oxdeadbeef",
wantErr: true,
},
{
name: "errors when id is too long",
TraceID: "1234567890abcdef1234567890abcdef1",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &span{
TraceID: tt.TraceID,
}
got, err := s.Trace()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}
func Test_span_SpanID(t *testing.T) {
tests := []struct {
name string
ID string
want string
wantErr bool
}{
{
name: "Span IDs cannot be null",
ID: "",
wantErr: true,
},
{
name: "validates known id correctly",
ID: "b26412d1ac16767d",
want: "b26412d1ac16767d",
},
{
name: "validates hex string correctly",
ID: "deadbeef",
want: "deadbeef",
},
{
name: "errors when string isn't hex",
ID: "oxdeadbeef",
wantErr: true,
},
{
name: "errors when id is too long",
ID: "1234567890abcdef1",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &span{
ID: tt.ID,
}
got, err := s.SpanID()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}
func Test_span_Parent(t *testing.T) {
tests := []struct {
name string
ParentID string
want string
wantErr bool
}{
{
name: "when there is no parent return empty string",
ParentID: "",
want: "",
},
{
name: "validates hex string correctly",
ParentID: "deadbeef",
want: "deadbeef",
},
{
name: "errors when string isn't hex",
ParentID: "oxdeadbeef",
wantErr: true,
},
{
name: "errors when parent id is too long",
ParentID: "1234567890abcdef1",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &span{
ParentID: tt.ParentID,
}
got, err := s.Parent()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}
func Test_span_Timestamp(t *testing.T) {
tests := []struct {
name string
Time *int64
want time.Time
}{
{
name: "converts to microseconds",
Time: func(i int64) *int64 { return &i }(3000000),
want: time.Unix(3, 0).UTC(),
},
{
name: "nil time should be zero time",
Time: nil,
want: time.Time{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &span{
Time: tt.Time,
}
require.Equal(t, tt.want, s.Timestamp())
})
}
}
func Test_span_Duration(t *testing.T) {
tests := []struct {
name string
dur *int64
want time.Duration
}{
{
name: "converts from 3 microseconds",
dur: func(i int64) *int64 { return &i }(3000000),
want: 3 * time.Second,
},
{
name: "nil time should be zero duration",
dur: nil,
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &span{
Dur: tt.dur,
}
require.Equal(t, tt.want, s.Duration())
})
}
}
func Test_annotation(t *testing.T) {
type fields struct {
Endpoint *endpoint
Time int64
Val string
}
tests := []struct {
name string
fields fields
tm time.Time
val string
endpoint *endpoint
}{
{
name: "returns all fields",
fields: fields{
Time: 3000000,
Val: "myvalue",
Endpoint: &endpoint{
ServiceName: "myservice",
Ipv4: "127.0.0.1",
Port: 443,
},
},
tm: time.Unix(3, 0).UTC(),
val: "myvalue",
endpoint: &endpoint{
ServiceName: "myservice",
Ipv4: "127.0.0.1",
Port: 443,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
an := annotation(tt.fields)
a := &an
require.Equal(t, tt.tm, a.Timestamp())
require.Equal(t, tt.val, a.Value())
require.Equal(t, tt.endpoint, a.Host())
})
}
}
func Test_binaryAnnotation(t *testing.T) {
type fields struct {
K string
V json.RawMessage
Type string
Endpoint *endpoint
}
tests := []struct {
name string
fields fields
key string
value string
endpoint *endpoint
}{
{
name: "returns all fields",
fields: fields{
K: "key",
V: json.RawMessage(`"value"`),
Endpoint: &endpoint{
ServiceName: "myservice",
Ipv4: "127.0.0.1",
Port: 443,
},
},
key: "key",
value: "value",
endpoint: &endpoint{
ServiceName: "myservice",
Ipv4: "127.0.0.1",
Port: 443,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bin := binaryAnnotation(tt.fields)
b := &bin
require.Equal(t, tt.key, b.Key())
require.Equal(t, tt.value, b.Value())
require.Equal(t, tt.endpoint, b.Host())
})
}
}
func Test_endpoint_Host(t *testing.T) {
type fields struct {
Ipv4 string
Port int
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "with port",
fields: fields{
Ipv4: "127.0.0.1",
Port: 443,
},
want: "127.0.0.1:443",
},
{
name: "no port",
fields: fields{
Ipv4: "127.0.0.1",
},
want: "127.0.0.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &endpoint{
Ipv4: tt.fields.Ipv4,
Port: tt.fields.Port,
}
require.Equal(t, tt.want, e.Host())
})
}
}
func Test_endpoint_Name(t *testing.T) {
tests := []struct {
name string
ServiceName string
want string
}{
{
name: "has service name",
ServiceName: "myservicename",
want: "myservicename",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &endpoint{
ServiceName: tt.ServiceName,
}
require.Equal(t, tt.want, e.Name())
})
}
}
func TestTraceIDFromString(t *testing.T) {
tests := []struct {
name string
s string
want string
wantErr bool
}{
{
name: "Convert hex string id",
s: "6b221d5bc9e6496c",
want: "6b221d5bc9e6496c",
},
{
name: "error : id too long",
s: "1234567890abcdef1234567890abcdef1",
wantErr: true,
},
{
name: "error : not parsable",
s: "howdyhowdyhowdy",
wantErr: true,
},
{
name: "Convert hex string with high/low",
s: "48485a3953bb61246b221d5bc9e6496c",
want: "48485a3953bb61246b221d5bc9e6496c",
},
{
name: "errors in high",
s: "ERR85a3953bb61246b221d5bc9e6496c",
wantErr: true,
},
{
name: "errors in low",
s: "48485a3953bb61246b221d5bc9e64ERR",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := traceIDFromString(tt.s)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}
func TestIDFromString(t *testing.T) {
tests := []struct {
name string
s string
want string
wantErr bool
}{
{
name: "validates hex string id",
s: "6b221d5bc9e6496c",
want: "6b221d5bc9e6496c",
},
{
name: "error : id too long",
s: "1234567890abcdef1",
wantErr: true,
},
{
name: "error : not parsable",
s: "howdyhowdyhowdy",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := idFromString(tt.s)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}

View file

@ -0,0 +1,5 @@
// Code generated by Thrift Compiler (0.14.2). DO NOT EDIT.
package zipkincore
var GoUnusedProtection__ int

View file

@ -0,0 +1,48 @@
// Code generated by Thrift Compiler (0.14.2). DO NOT EDIT.
package zipkincore
import (
"bytes"
"context"
"fmt"
"time"
"github.com/apache/thrift/lib/go/thrift"
)
// (needed to ensure safety because of naive import list construction.)
var _ = thrift.ZERO
var _ = fmt.Printf
var _ = context.Background
var _ = time.Now
var _ = bytes.Equal
const CLIENT_SEND = "cs"
const CLIENT_RECV = "cr"
const SERVER_SEND = "ss"
const SERVER_RECV = "sr"
const MESSAGE_SEND = "ms"
const MESSAGE_RECV = "mr"
const WIRE_SEND = "ws"
const WIRE_RECV = "wr"
const CLIENT_SEND_FRAGMENT = "csf"
const CLIENT_RECV_FRAGMENT = "crf"
const SERVER_SEND_FRAGMENT = "ssf"
const SERVER_RECV_FRAGMENT = "srf"
const HTTP_HOST = "http.host"
const HTTP_METHOD = "http.method"
const HTTP_PATH = "http.path"
const HTTP_ROUTE = "http.route"
const HTTP_URL = "http.url"
const HTTP_STATUS_CODE = "http.status_code"
const HTTP_REQUEST_SIZE = "http.request.size"
const HTTP_RESPONSE_SIZE = "http.response.size"
const LOCAL_COMPONENT = "lc"
const ERROR = "error"
const CLIENT_ADDR = "ca"
const SERVER_ADDR = "sa"
const MESSAGE_ADDR = "ma"
func init() {
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,220 @@
package thrift
import (
"context"
"encoding/binary"
"errors"
"fmt"
"net"
"strconv"
"time"
"github.com/apache/thrift/lib/go/thrift"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec/thrift/gen-go/zipkincore"
)
// Thrift decodes binary data to create a Trace
type Thrift struct{}
// Decode unmarshals and validates bytes in thrift format
func (*Thrift) Decode(octets []byte) ([]codec.Span, error) {
spans, err := unmarshalThrift(octets)
if err != nil {
return nil, err
}
res := make([]codec.Span, 0, len(spans))
for _, s := range spans {
res = append(res, &span{s})
}
return res, nil
}
// unmarshalThrift converts raw bytes in thrift format to a slice of spans
func unmarshalThrift(body []byte) ([]*zipkincore.Span, error) {
buffer := thrift.NewTMemoryBuffer()
buffer.Write(body)
transport := thrift.NewTBinaryProtocolConf(buffer, nil)
_, size, err := transport.ReadListBegin(context.Background())
if err != nil {
return nil, err
}
spans := make([]*zipkincore.Span, 0, size)
for i := 0; i < size; i++ {
zs := &zipkincore.Span{}
if err := zs.Read(context.Background(), transport); err != nil {
return nil, err
}
spans = append(spans, zs)
}
if err := transport.ReadListEnd(context.Background()); err != nil {
return nil, err
}
return spans, nil
}
var _ codec.Endpoint = &endpoint{}
type endpoint struct {
*zipkincore.Endpoint
}
// Host returns the host address of the endpoint as a string.
func (e *endpoint) Host() string {
ipv4 := func(addr int32) string {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, uint32(addr))
return net.IP(buf).String()
}
if e.Endpoint == nil {
return ipv4(int32(0))
}
if e.Endpoint.GetPort() == 0 {
return ipv4(e.Endpoint.GetIpv4())
}
// Zipkin uses a signed int16 for the port, but, warns us that they actually treat it
// as an unsigned int16. So, we convert from int16 to int32 followed by taking & 0xffff
// to convert from signed to unsigned
// https://github.com/openzipkin/zipkin/blob/57dc2ec9c65fe6144e401c0c933b4400463a69df/zipkin/src/main/java/zipkin/Endpoint.java#L44
return ipv4(e.Endpoint.GetIpv4()) + ":" + strconv.FormatInt(int64(int(e.Endpoint.GetPort())&0xffff), 10)
}
// Name returns the name of the service associated with the endpoint as a string.
func (e *endpoint) Name() string {
if e.Endpoint == nil {
return codec.DefaultServiceName
}
return e.Endpoint.GetServiceName()
}
var _ codec.BinaryAnnotation = &binaryAnnotation{}
type binaryAnnotation struct {
*zipkincore.BinaryAnnotation
}
// Key returns the key of the binary annotation as a string.
func (b *binaryAnnotation) Key() string {
return b.BinaryAnnotation.GetKey()
}
// Value returns the value of the binary annotation as a string.
func (b *binaryAnnotation) Value() string {
return string(b.BinaryAnnotation.GetValue())
}
// Host returns the endpoint associated with the binary annotation as a codec.Endpoint.
func (b *binaryAnnotation) Host() codec.Endpoint {
if b.BinaryAnnotation.Host == nil {
return nil
}
return &endpoint{b.BinaryAnnotation.Host}
}
var _ codec.Annotation = &annotation{}
type annotation struct {
*zipkincore.Annotation
}
// Timestamp returns the timestamp of the annotation as a time.Time object.
func (a *annotation) Timestamp() time.Time {
ts := a.Annotation.GetTimestamp()
if ts == 0 {
return time.Time{}
}
return codec.MicroToTime(ts)
}
// Value returns the value of the annotation as a string.
func (a *annotation) Value() string {
return a.Annotation.GetValue()
}
// Host returns the endpoint associated with the annotation as a codec.Endpoint.
func (a *annotation) Host() codec.Endpoint {
if a.Annotation.Host == nil {
return nil
}
return &endpoint{a.Annotation.Host}
}
var _ codec.Span = &span{}
type span struct {
*zipkincore.Span
}
// Trace returns the trace ID of the span and an error if the trace ID is invalid.
func (s *span) Trace() (string, error) {
if s.Span.GetTraceIDHigh() == 0 && s.Span.GetTraceID() == 0 {
return "", errors.New("span does not have a trace ID")
}
if s.Span.GetTraceIDHigh() == 0 {
return fmt.Sprintf("%x", s.Span.GetTraceID()), nil
}
return fmt.Sprintf("%x%016x", s.Span.GetTraceIDHigh(), s.Span.GetTraceID()), nil
}
// SpanID returns the span ID of the span and an error if the span ID is invalid.
func (s *span) SpanID() (string, error) {
return formatID(s.Span.GetID()), nil
}
// Parent returns the parent span ID of the span and an error if the parent ID is invalid.
func (s *span) Parent() (string, error) {
id := s.Span.GetParentID()
if id != 0 {
return formatID(id), nil
}
return "", nil
}
// Name returns the name of the span.
func (s *span) Name() string {
return s.Span.GetName()
}
// Annotations returns the annotations of the span as a slice of codec.Annotation.
func (s *span) Annotations() []codec.Annotation {
res := make([]codec.Annotation, 0, len(s.Span.Annotations))
for _, ann := range s.Span.Annotations {
res = append(res, &annotation{ann})
}
return res
}
// BinaryAnnotations returns the binary annotations of the span as a slice of codec.BinaryAnnotation and an error if the binary annotations cannot be retrieved.
func (s *span) BinaryAnnotations() ([]codec.BinaryAnnotation, error) {
res := make([]codec.BinaryAnnotation, 0, len(s.Span.BinaryAnnotations))
for _, ann := range s.Span.BinaryAnnotations {
res = append(res, &binaryAnnotation{ann})
}
return res, nil
}
// Timestamp returns the timestamp of the span as a time.Time object.
func (s *span) Timestamp() time.Time {
ts := s.Span.GetTimestamp()
if ts == 0 {
return time.Time{}
}
return codec.MicroToTime(ts)
}
// Duration returns the duration of the span as a time.Duration object.
func (s *span) Duration() time.Duration {
return time.Duration(s.Span.GetDuration()) * time.Microsecond
}
// formatID formats the given ID as a hexadecimal string.
func formatID(id int64) string {
return strconv.FormatInt(id, 16)
}

View file

@ -0,0 +1,206 @@
package thrift
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec/thrift/gen-go/zipkincore"
)
func Test_endpointHost(t *testing.T) {
type args struct {
h *zipkincore.Endpoint
}
tests := []struct {
name string
args args
want string
}{
{
name: "Host Found",
args: args{
h: &zipkincore.Endpoint{
Ipv4: 1234,
Port: 8888,
},
},
want: "0.0.4.210:8888",
},
{
name: "No Host",
args: args{
h: nil,
},
want: "0.0.0.0",
},
{
name: "int overflow zipkin uses an int16 type as an unsigned int 16.",
args: args{
h: &zipkincore.Endpoint{
Ipv4: 1234,
Port: -1,
},
},
want: "0.0.4.210:65535",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := endpoint{tt.args.h}
require.Equal(t, tt.want, e.Host())
})
}
}
func Test_endpointName(t *testing.T) {
type args struct {
h *zipkincore.Endpoint
}
tests := []struct {
name string
args args
want string
}{
{
name: "Found ServiceName",
args: args{
h: &zipkincore.Endpoint{
ServiceName: "zipkin",
},
},
want: "zipkin",
},
{
name: "No ServiceName",
args: args{
h: nil,
},
want: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := endpoint{tt.args.h}
require.Equal(t, tt.want, e.Name())
})
}
}
func TestUnmarshalThrift(t *testing.T) {
addr := func(i int64) *int64 { return &i }
tests := []struct {
name string
filename string
want []*zipkincore.Span
wantErr bool
}{
{
name: "threespans",
filename: "../../testdata/threespans.dat",
want: []*zipkincore.Span{
{
TraceID: 2505404965370368069,
Name: "Child",
ID: 8090652509916334619,
ParentID: addr(22964302721410078),
Timestamp: addr(1498688360851331),
Duration: addr(53106),
Annotations: make([]*zipkincore.Annotation, 0),
BinaryAnnotations: []*zipkincore.BinaryAnnotation{
{
Key: "lc",
AnnotationType: zipkincore.AnnotationType_STRING,
Value: []byte("trivial"),
Host: &zipkincore.Endpoint{
Ipv4: 2130706433,
ServiceName: "trivial",
},
},
},
},
{
TraceID: 2505404965370368069,
Name: "Child",
ID: 103618986556047333,
ParentID: addr(22964302721410078),
Timestamp: addr(1498688360904552),
Duration: addr(50410),
Annotations: make([]*zipkincore.Annotation, 0),
BinaryAnnotations: []*zipkincore.BinaryAnnotation{
{
Key: "lc",
AnnotationType: zipkincore.AnnotationType_STRING,
Value: []byte("trivial"),
Host: &zipkincore.Endpoint{
Ipv4: 2130706433,
ServiceName: "trivial",
},
},
},
},
{
TraceID: 2505404965370368069,
Name: "Parent",
ID: 22964302721410078,
Timestamp: addr(1498688360851318),
Duration: addr(103680),
Annotations: []*zipkincore.Annotation{
{
Timestamp: 1498688360851325,
Value: "Starting child #0",
Host: &zipkincore.Endpoint{
Ipv4: 2130706433,
ServiceName: "trivial",
},
},
{
Timestamp: 1498688360904545,
Value: "Starting child #1",
Host: &zipkincore.Endpoint{
Ipv4: 2130706433,
ServiceName: "trivial",
},
},
{
Timestamp: 1498688360954992,
Value: "A Log",
Host: &zipkincore.Endpoint{
Ipv4: 2130706433,
ServiceName: "trivial",
},
},
},
BinaryAnnotations: []*zipkincore.BinaryAnnotation{
{
Key: "lc",
AnnotationType: zipkincore.AnnotationType_STRING,
Value: []byte("trivial"),
Host: &zipkincore.Endpoint{
Ipv4: 2130706433,
ServiceName: "trivial",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dat, err := os.ReadFile(tt.filename)
if err != nil {
t.Fatalf("Could not find file %s\n", tt.filename)
}
got, err := unmarshalThrift(dat)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}

View file

@ -0,0 +1,79 @@
package zipkin
import (
"strings"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/trace"
)
// lineProtocolConverter implements the recorder interface;
// it is a type meant to encapsulate the storage of zipkin tracing data in telegraf as line protocol.
type lineProtocolConverter struct {
acc telegraf.Accumulator
}
// newLineProtocolConverter returns an instance of lineProtocolConverter that will add to the given telegraf.Accumulator
func newLineProtocolConverter(acc telegraf.Accumulator) *lineProtocolConverter {
return &lineProtocolConverter{
acc: acc,
}
}
// record is lineProtocolConverter's implementation of the record method of the recorder interface;
// it takes a trace as input, and adds it to an internal telegraf.Accumulator.
func (l *lineProtocolConverter) record(t trace.Trace) error {
for _, s := range t {
fields := map[string]interface{}{
"duration_ns": s.Duration.Nanoseconds(),
}
tags := map[string]string{
"id": s.ID,
"parent_id": s.ParentID,
"trace_id": s.TraceID,
"name": formatName(s.Name),
"service_name": formatName(s.ServiceName),
}
l.acc.AddFields("zipkin", fields, tags, s.Timestamp)
for _, a := range s.Annotations {
tags := map[string]string{
"id": s.ID,
"parent_id": s.ParentID,
"trace_id": s.TraceID,
"name": formatName(s.Name),
"service_name": formatName(a.ServiceName),
"annotation": a.Value,
"endpoint_host": a.Host,
}
l.acc.AddFields("zipkin", fields, tags, s.Timestamp)
}
for _, b := range s.BinaryAnnotations {
tags := map[string]string{
"id": s.ID,
"parent_id": s.ParentID,
"trace_id": s.TraceID,
"name": formatName(s.Name),
"service_name": formatName(b.ServiceName),
"annotation": b.Value,
"endpoint_host": b.Host,
"annotation_key": b.Key,
}
l.acc.AddFields("zipkin", fields, tags, s.Timestamp)
}
}
return nil
}
func (l *lineProtocolConverter) error(err error) {
l.acc.AddError(err)
}
// formatName formats name and service name Zipkin forces span and service names to be lowercase:
// https://github.com/openzipkin/zipkin/pull/805
func formatName(name string) string {
return strings.ToLower(name)
}

View file

@ -0,0 +1,348 @@
package zipkin
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/trace"
"github.com/influxdata/telegraf/testutil"
)
func TestLineProtocolConverter_Record(t *testing.T) {
mockAcc := testutil.Accumulator{}
type fields struct {
acc telegraf.Accumulator
}
type args struct {
t trace.Trace
}
tests := []struct {
name string
fields fields
args args
wantErr bool
want []testutil.Metric
}{
{
name: "threespan",
fields: fields{
acc: &mockAcc,
},
args: args{
t: trace.Trace{
{
ID: "8090652509916334619",
TraceID: "2505404965370368069",
Name: "Child",
ParentID: "22964302721410078",
Timestamp: time.Unix(0, 1498688360851331000).UTC(),
Duration: time.Duration(53106) * time.Microsecond,
ServiceName: "trivial",
BinaryAnnotations: []trace.BinaryAnnotation{
{
Key: "lc",
Value: "dHJpdmlhbA==",
Host: "2130706433:0",
ServiceName: "trivial",
},
},
},
{
ID: "103618986556047333",
TraceID: "2505404965370368069",
Name: "Child",
ParentID: "22964302721410078",
Timestamp: time.Unix(0, 1498688360904552000).UTC(),
Duration: time.Duration(50410) * time.Microsecond,
ServiceName: "trivial",
BinaryAnnotations: []trace.BinaryAnnotation{
{
Key: "lc",
Value: "dHJpdmlhbA==",
Host: "2130706433:0",
ServiceName: "trivial",
},
},
},
{
ID: "22964302721410078",
TraceID: "2505404965370368069",
Name: "Parent",
ParentID: "22964302721410078",
Timestamp: time.Unix(0, 1498688360851318000).UTC(),
Duration: time.Duration(103680) * time.Microsecond,
ServiceName: "trivial",
Annotations: []trace.Annotation{
{
Timestamp: time.Unix(0, 1498688360851325000).UTC(),
Value: "Starting child #0",
Host: "2130706433:0",
ServiceName: "trivial",
},
{
Timestamp: time.Unix(0, 1498688360904545000).UTC(),
Value: "Starting child #1",
Host: "2130706433:0",
ServiceName: "trivial",
},
{
Timestamp: time.Unix(0, 1498688360954992000).UTC(),
Value: "A Log",
Host: "2130706433:0",
ServiceName: "trivial",
},
},
BinaryAnnotations: []trace.BinaryAnnotation{
{
Key: "lc",
Value: "dHJpdmlhbA==",
Host: "2130706433:0",
ServiceName: "trivial",
},
},
},
},
},
want: []testutil.Metric{
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "8090652509916334619",
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"service_name": "trivial",
"name": "child",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(53106) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851331000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "8090652509916334619",
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"name": "child",
"service_name": "trivial",
"annotation": "dHJpdmlhbA==",
"endpoint_host": "2130706433:0",
"annotation_key": "lc",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(53106) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851331000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "103618986556047333",
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"service_name": "trivial",
"name": "child",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(50410) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360904552000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "103618986556047333",
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"name": "child",
"service_name": "trivial",
"annotation": "dHJpdmlhbA==",
"endpoint_host": "2130706433:0",
"annotation_key": "lc",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(50410) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360904552000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "22964302721410078",
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"service_name": "trivial",
"name": "parent",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"service_name": "trivial",
"annotation": "Starting child #0",
"endpoint_host": "2130706433:0",
"id": "22964302721410078",
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"name": "parent",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"service_name": "trivial",
"annotation": "Starting child #1",
"endpoint_host": "2130706433:0",
"id": "22964302721410078",
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"name": "parent",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"parent_id": "22964302721410078",
"trace_id": "2505404965370368069",
"name": "parent",
"service_name": "trivial",
"annotation": "A Log",
"endpoint_host": "2130706433:0",
"id": "22964302721410078",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"trace_id": "2505404965370368069",
"service_name": "trivial",
"annotation": "dHJpdmlhbA==",
"annotation_key": "lc",
"id": "22964302721410078",
"parent_id": "22964302721410078",
"name": "parent",
"endpoint_host": "2130706433:0",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
},
wantErr: false,
},
// Test data from distributed trace repo sample json
// https://github.com/mattkanwisher/distributedtrace/blob/master/testclient/sample.json
{
name: "distributed_trace_sample",
fields: fields{
acc: &mockAcc,
},
args: args{
t: trace.Trace{
{
ID: "6802735349851856000",
TraceID: "0:6802735349851856000",
Name: "main.dud",
ParentID: "6802735349851856000",
Timestamp: time.Unix(1, 0).UTC(),
Duration: 1,
ServiceName: "trivial",
Annotations: []trace.Annotation{
{
Timestamp: time.Unix(0, 1433330263415871000).UTC(),
Value: "cs",
Host: "0:9410",
ServiceName: "go-zipkin-testclient",
},
},
},
},
},
want: []testutil.Metric{
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "6802735349851856000",
"parent_id": "6802735349851856000",
"trace_id": "0:6802735349851856000",
"name": "main.dud",
"service_name": "trivial",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(1) * time.Nanosecond).Nanoseconds(),
},
Time: time.Unix(1, 0).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "cs",
"endpoint_host": "0:9410",
"id": "6802735349851856000",
"parent_id": "6802735349851856000",
"trace_id": "0:6802735349851856000",
"name": "main.dud",
"service_name": "go-zipkin-testclient",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(1) * time.Nanosecond).Nanoseconds(),
},
Time: time.Unix(1, 0).UTC(),
Type: telegraf.Untyped,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockAcc.ClearMetrics()
l := &lineProtocolConverter{
acc: tt.fields.acc,
}
err := l.record(tt.args.t)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
got := make([]testutil.Metric, 0, len(mockAcc.Metrics))
for _, metric := range mockAcc.Metrics {
got = append(got, *metric)
}
require.Equal(t, tt.want, got)
})
}
}

View file

@ -0,0 +1,141 @@
package zipkin
import (
"compress/gzip"
"fmt"
"io"
"mime"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec"
json_v1 "github.com/influxdata/telegraf/plugins/inputs/zipkin/codec/jsonV1"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/codec/thrift"
)
// spanHandler is an implementation of a handler which accepts zipkin thrift span data and sends it to the recorder
type spanHandler struct {
path string
recorder recorder
}
func newSpanHandler(path string) *spanHandler {
return &spanHandler{
path: path,
}
}
func cors(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set(`Access-Control-Allow-Origin`, origin)
w.Header().Set(`Access-Control-Allow-Methods`, strings.Join([]string{
`OPTIONS`,
`POST`,
}, ", "))
w.Header().Set(`Access-Control-Allow-Headers`, strings.Join([]string{
`Accept`,
`Accept-Encoding`,
`Content-Length`,
`Content-Type`,
}, ", "))
w.Header().Set(`Access-Control-Expose-Headers`, strings.Join([]string{
`Date`,
}, ", "))
}
if r.Method == "OPTIONS" {
return
}
next.ServeHTTP(w, r)
}
}
// register implements the Service interface. Register accepts zipkin thrift data
// POSTed to the path of the mux router
func (s *spanHandler) register(router *mux.Router, recorder recorder) error {
handler := cors(http.HandlerFunc(s.spans))
router.Handle(s.path, handler).Methods("POST", "OPTIONS")
s.recorder = recorder
return nil
}
// spans handles zipkin thrift spans
func (s *spanHandler) spans(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body := r.Body
var err error
// Handle gzip decoding of the body
if r.Header.Get("Content-Encoding") == "gzip" {
body, err = gzip.NewReader(r.Body)
if err != nil {
s.recorder.error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer body.Close()
}
decoder, err := contentDecoder(r)
if err != nil {
s.recorder.error(err)
w.WriteHeader(http.StatusUnsupportedMediaType)
}
octets, err := io.ReadAll(body)
if err != nil {
s.recorder.error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
spans, err := decoder.Decode(octets)
if err != nil {
s.recorder.error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
trace, err := codec.NewTrace(spans)
if err != nil {
s.recorder.error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err = s.recorder.record(trace); err != nil {
s.recorder.error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// contentDecoder returns a Decoder that is able to produce Traces from bytes.
// Failure should yield an HTTP 415 (`http.StatusUnsupportedMediaType`)
// If a Content-Type is not set, zipkin assumes application/json
func contentDecoder(r *http.Request) (codec.Decoder, error) {
contentType := r.Header.Get("Content-Type")
if contentType == "" {
return &json_v1.JSON{}, nil
}
for _, v := range strings.Split(contentType, ",") {
t, _, err := mime.ParseMediaType(v)
if err != nil {
break
}
if t == "application/json" {
return &json_v1.JSON{}, nil
} else if t == "application/x-thrift" {
return &thrift.Thrift{}, nil
}
}
return nil, fmt.Errorf("unknown Content-Type: %s", contentType)
}

View file

@ -0,0 +1,133 @@
package zipkin
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/trace"
)
type mockRecorder struct {
data trace.Trace
err error
}
func (m *mockRecorder) record(t trace.Trace) error {
m.data = t
return nil
}
func (m *mockRecorder) error(err error) {
m.err = err
}
func TestSpanHandler(t *testing.T) {
dat, err := os.ReadFile("testdata/threespans.dat")
if err != nil {
t.Fatalf("Could not find file %s\n", "testdata/threespans.dat")
}
w := httptest.NewRecorder()
r := httptest.NewRequest(
"POST",
"http://server.local/api/v1/spans",
io.NopCloser(
bytes.NewReader(dat)))
r.Header.Set("Content-Type", "application/x-thrift")
handler := newSpanHandler("/api/v1/spans")
mockRecorder := &mockRecorder{}
handler.recorder = mockRecorder
handler.spans(w, r)
require.Equal(t, http.StatusNoContent, w.Code)
got := mockRecorder.data
parentID := strconv.FormatInt(22964302721410078, 16)
want := trace.Trace{
{
Name: "Child",
ID: "7047c59776af8a1b",
TraceID: "22c4fc8ab3669045",
ParentID: parentID,
Timestamp: time.Unix(0, 1498688360851331*int64(time.Microsecond)).UTC(),
Duration: time.Duration(53106) * time.Microsecond,
ServiceName: "trivial",
Annotations: make([]trace.Annotation, 0),
BinaryAnnotations: []trace.BinaryAnnotation{
{
Key: "lc",
Value: "trivial",
Host: "127.0.0.1",
ServiceName: "trivial",
},
},
},
{
Name: "Child",
ID: "17020eb55a8bfe5",
TraceID: "22c4fc8ab3669045",
ParentID: parentID,
Timestamp: time.Unix(0, 1498688360904552*int64(time.Microsecond)).UTC(),
Duration: time.Duration(50410) * time.Microsecond,
ServiceName: "trivial",
Annotations: make([]trace.Annotation, 0),
BinaryAnnotations: []trace.BinaryAnnotation{
{
Key: "lc",
Value: "trivial",
Host: "127.0.0.1",
ServiceName: "trivial",
},
},
},
{
Name: "Parent",
ID: "5195e96239641e",
TraceID: "22c4fc8ab3669045",
ParentID: parentID,
Timestamp: time.Unix(0, 1498688360851318*int64(time.Microsecond)).UTC(),
Duration: time.Duration(103680) * time.Microsecond,
ServiceName: "trivial",
Annotations: []trace.Annotation{
{
Timestamp: time.Unix(0, 1498688360851325*int64(time.Microsecond)).UTC(),
Value: "Starting child #0",
Host: "127.0.0.1",
ServiceName: "trivial",
},
{
Timestamp: time.Unix(0, 1498688360904545*int64(time.Microsecond)).UTC(),
Value: "Starting child #1",
Host: "127.0.0.1",
ServiceName: "trivial",
},
{
Timestamp: time.Unix(0, 1498688360954992*int64(time.Microsecond)).UTC(),
Value: "A Log",
Host: "127.0.0.1",
ServiceName: "trivial",
},
},
BinaryAnnotations: []trace.BinaryAnnotation{
{
Key: "lc",
Value: "trivial",
Host: "127.0.0.1",
ServiceName: "trivial",
},
},
},
}
require.Equal(t, want, got)
}

View file

@ -0,0 +1,12 @@
# Gather data from a Zipkin server including trace and timing data
[[inputs.zipkin]]
## URL path for span data
# path = "/api/v1/spans"
## Port on which Telegraf listens
# port = 9411
## Maximum duration before timing out read of the request
# read_timeout = "10s"
## Maximum duration before timing out write of the response
# write_timeout = "10s"

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,188 @@
[
{
"traceId": "7312f822d43d0fd8",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parentId": "7312f822d43d0fd8",
"annotations": [
{
"timestamp": 1503031538791000,
"value": "sr",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"timestamp": 1503031538794000,
"value": "ss",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
}
],
"binaryAnnotations": [
{
"key": "mvc.controller.class",
"value": "Demo2Application",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "mvc.controller.method",
"value": "hi2",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "spring.instance_id",
"value": "192.168.0.8:test:8010",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
}
]
},
{
"traceId": "7312f822d43d0fd8",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parentId": "7312f822d43d0fd8",
"timestamp": 1503031538786000,
"duration": 10000,
"annotations": [
{
"timestamp": 1503031538786000,
"value": "cs",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"timestamp": 1503031538796000,
"value": "cr",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
}
],
"binaryAnnotations": [
{
"key": "http.host",
"value": "localhost",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "http.method",
"value": "GET",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "http.path",
"value": "/hi2",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "http.url",
"value": "http://localhost:8010/hi2",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "spring.instance_id",
"value": "192.168.0.8:test:8010",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
}
]
},
{
"traceId": "7312f822d43d0fd8",
"id": "7312f822d43d0fd8",
"name": "http:/hi",
"timestamp": 1503031538778000,
"duration": 23393,
"annotations": [
{
"timestamp": 1503031538778000,
"value": "sr",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"timestamp": 1503031538801000,
"value": "ss",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
}
],
"binaryAnnotations": [
{
"key": "mvc.controller.class",
"value": "Demo2Application",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "mvc.controller.method",
"value": "hi",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
},
{
"key": "spring.instance_id",
"value": "192.168.0.8:test:8010",
"endpoint": {
"serviceName": "test",
"ipv4": "192.168.0.8",
"port": 8010
}
}
]
}
]

View file

@ -0,0 +1,407 @@
[
{
"trace_id": 243463817635710260,
"name": "Concat",
"id": 3383422996321511664,
"parent_id": 4574092882326506380,
"annotations": [
{
"timestamp": 1499817952283903,
"value": "cs",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"timestamp": 1499817952286792,
"value": "cr",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
}
],
"binary_annotations": [
{
"key": "http.path",
"value": "L2NvbmNhdC8=",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "http.url",
"value": "aHR0cDovL2xvY2FsaG9zdDo2MTAwMS9jb25jYXQv",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "peer.hostname",
"value": "bG9jYWxob3N0",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "span.kind",
"value": "Y2xpZW50",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "http.method",
"value": "R0VU",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "http.host",
"value": "bG9jYWxob3N0OjYxMDAx",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
}
],
"timestamp": 1499817952283903,
"duration": 2888,
"trace_id_high": 8269862291023777619
},
{
"trace_id": 243463817635710260,
"name": "Sum",
"id": 6036416808826525494,
"parent_id": 4574092882326506380,
"annotations": [
{
"timestamp": 1499817952286828,
"value": "cs",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"timestamp": 1499817952333847,
"value": "cr",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
}
],
"binary_annotations": [
{
"key": "span.kind",
"value": "Y2xpZW50",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "http.method",
"value": "R0VU",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "http.host",
"value": "bG9jYWxob3N0OjYxMDAx",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "http.path",
"value": "L3N1bS8=",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "http.url",
"value": "aHR0cDovL2xvY2FsaG9zdDo2MTAwMS9zdW0v",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"key": "peer.hostname",
"value": "bG9jYWxob3N0",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
}
],
"timestamp": 1499817952286828,
"duration": 47019,
"trace_id_high": 8269862291023777619
},
{
"trace_id": 243463817635710260,
"name": "Run",
"id": 4574092882326506380,
"annotations": [
{
"timestamp": 1499817952283897,
"value": "Call Concat",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
},
{
"timestamp": 1499817952286824,
"value": "Call Sum",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
}
],
"binary_annotations": [
{
"key": "lc",
"value": "Y2xp",
"annotation_type": "STRING",
"host": {
"ipv4": 0,
"port": 0,
"service_name": "cli"
}
}
],
"timestamp": 1499817952283881,
"duration": 50014,
"trace_id_high": 8269862291023777619
}
]
[
{
"trace_id": 243463817635710260,
"name": "myComplexQuery",
"id": 4254041670140233539,
"parent_id": 8633460035494236932,
"annotations": [
{
"timestamp": 1499817952307418,
"value": "cs",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"timestamp": 1499817952331909,
"value": "cr",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
}
],
"binary_annotations": [
{
"key": "sa",
"value": "UG9zdGdyZVNRTA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": 5432,
"service_name": "PostgreSQL",
"ipv6": "AAAAAAAAAAAAAAAAAAAAAQ=="
}
},
{
"key": "query",
"value": "U0VMRUNUIHJlY2lwZXMgRlJPTSBjb29rYm9vayBXSEVSRSB0b3BpYyA9ICd3b3JsZCBkb21pbmF0aW9uJw==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "span.kind",
"value": "cmVzb3VyY2U=",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "peer.service",
"value": "UG9zdGdyZVNRTA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "peer.hostname",
"value": "bG9jYWxob3N0",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "peer.port",
"value": "AAAVOA==",
"annotation_type": "I32",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
}
],
"timestamp": 1499817952307418,
"duration": 24491,
"trace_id_high": 8269862291023777619
},
{
"trace_id": 243463817635710260,
"name": "Sum",
"id": 8633460035494236932,
"parent_id": 6036416808826525494,
"annotations": [
{
"timestamp": 1499817952287147,
"value": "sr",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"timestamp": 1499817952333348,
"value": "ss",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"timestamp": 1499817952296675,
"value": "MyEventAnnotation",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
}
],
"binary_annotations": [
{
"key": "span.kind",
"value": "c2VydmVy",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "serverSide",
"value": "aGVyZQ==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "service",
"value": "c3ZjMg==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "key1",
"value": "dmFsdWUx",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
},
{
"key": "key2",
"value": "AAAAAgAAAAA=",
"annotation_type": "I32",
"host": {
"ipv4": 2130706433,
"port": -4534,
"service_name": "svc2"
}
}
],
"trace_id_high": 8269862291023777619
}
]

View file

@ -0,0 +1,30 @@
[{
"trace_id": 6802735349851856000,
"name": "main.dud",
"id": 6802735349851856000,
"parent_id": null,
"annotations": [
{
"timestamp": 1433330263415871,
"value": "cs",
"host": {
"ipv4": 0,
"port": 9410,
"service_name": "go-zipkin-testclient"
},
"duration": null
},
{
"timestamp": 1433330263415872,
"value": "cr",
"host": {
"ipv4": 0,
"port": 9410,
"service_name": "go-zipkin-testclient"
},
"duration": null
}
],
"binary_annotations": [],
"debug": true
}]

View file

@ -0,0 +1,92 @@
[
{
"trace_id": 2505404965370368069,
"name": "Child",
"id": 8090652509916334619,
"parent_id": 22964302721410078,
"annotations": [],
"binary_annotations": [
{
"key": "lc",
"value": "dHJpdmlhbA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"timestamp": 1498688360851331,
"duration": 53106
},
{
"trace_id": 2505404965370368069,
"name": "Child",
"id": 103618986556047333,
"parent_id": 22964302721410078,
"annotations": [],
"binary_annotations": [
{
"key": "lc",
"value": "dHJpdmlhbA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"timestamp": 1498688360904552,
"duration": 50410
},
{
"trace_id": 2505404965370368069,
"name": "Parent",
"id": 22964302721410078,
"annotations": [
{
"timestamp": 1498688360851325,
"value": "Starting child #0",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
},
{
"timestamp": 1498688360904545,
"value": "Starting child #1",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
},
{
"timestamp": 1498688360954992,
"value": "A Log",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"binary_annotations": [
{
"key": "lc",
"value": "dHJpdmlhbA==",
"annotation_type": "STRING",
"host": {
"ipv4": 2130706433,
"port": 0,
"service_name": "trivial"
}
}
],
"timestamp": 1498688360851318,
"duration": 103680
}
]

Binary file not shown.

View file

@ -0,0 +1,41 @@
package trace
import (
"time"
)
// Trace is an array (or a series) of spans
type Trace []Span
// Span represents a specific zipkin span. It holds the majority of the same
// data as a zipkin span sent via the thrift protocol, but is presented in a
// format which is more straightforward for storage purposes.
type Span struct {
ID string
TraceID string // zipkin traceid high concat with traceid
Name string
ParentID string
ServiceName string
Timestamp time.Time // If zipkin input is nil then time.Now()
Duration time.Duration
Annotations []Annotation
BinaryAnnotations []BinaryAnnotation
}
// BinaryAnnotation represents a zipkin binary annotation. It contains
// all of the same fields as might be found in its zipkin counterpart.
type BinaryAnnotation struct {
Key string
Value string
Host string // annotation.endpoint.ipv4 + ":" + annotation.endpoint.port
ServiceName string
}
// Annotation represents an ordinary zipkin annotation. It contains the data fields
// which will become fields/tags in influxdb
type Annotation struct {
Timestamp time.Time
Value string
Host string // annotation.endpoint.ipv4 + ":" + annotation.endpoint.port
ServiceName string
}

View file

@ -0,0 +1,150 @@
//go:generate ../../../tools/readme_config_includer/generator
package zipkin
import (
"context"
_ "embed"
"fmt"
"net"
"net/http"
"strconv"
"sync"
"time"
"github.com/gorilla/mux"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/plugins/inputs/zipkin/trace"
)
//go:embed sample.conf
var sampleConfig string
var (
// defaultNetwork is the network to listen on; use only in tests.
defaultNetwork = "tcp"
)
const (
// defaultPort is the default port zipkin listens on, which zipkin implementations expect.
defaultPort = 9411
// defaultRoute is the default route zipkin uses, and zipkin implementations expect.
defaultRoute = "/api/v1/spans"
// defaultShutdownTimeout is the max amount of time telegraf will wait for the plugin to shut down
defaultShutdownTimeout = 5 * time.Second
defaultReadTimeout = 10 * time.Second
defaultWriteTimeout = 10 * time.Second
)
type Zipkin struct {
Port int `toml:"port"`
Path string `toml:"path"`
ReadTimeout config.Duration `toml:"read_timeout"`
WriteTimeout config.Duration `toml:"write_timeout"`
Log telegraf.Logger `toml:"-"`
address string
handler handler
server *http.Server
waitGroup *sync.WaitGroup
}
// recorder represents a type which can record zipkin trace data as well as any accompanying errors, and process that data.
type recorder interface {
record(trace.Trace) error
error(error)
}
// handler represents a type which can register itself with a router for http routing, and a recorder for trace data collection.
type handler interface {
register(router *mux.Router, recorder recorder) error
}
func (*Zipkin) SampleConfig() string {
return sampleConfig
}
func (z *Zipkin) Start(acc telegraf.Accumulator) error {
if z.ReadTimeout < config.Duration(time.Second) {
z.ReadTimeout = config.Duration(defaultReadTimeout)
}
if z.WriteTimeout < config.Duration(time.Second) {
z.WriteTimeout = config.Duration(defaultWriteTimeout)
}
z.handler = newSpanHandler(z.Path)
var wg sync.WaitGroup
z.waitGroup = &wg
router := mux.NewRouter()
converter := newLineProtocolConverter(acc)
if err := z.handler.register(router, converter); err != nil {
return err
}
z.server = &http.Server{
Handler: router,
ReadTimeout: time.Duration(z.ReadTimeout),
WriteTimeout: time.Duration(z.WriteTimeout),
}
addr := ":" + strconv.Itoa(z.Port)
ln, err := net.Listen(defaultNetwork, addr)
if err != nil {
return err
}
z.address = ln.Addr().String()
z.Log.Infof("Started the zipkin listener on %s", z.address)
wg.Add(1)
go func() {
defer wg.Done()
z.listen(ln, acc)
}()
return nil
}
func (*Zipkin) Gather(telegraf.Accumulator) error { return nil }
func (z *Zipkin) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), defaultShutdownTimeout)
defer z.waitGroup.Wait()
defer cancel()
z.server.Shutdown(ctx) //nolint:errcheck // Ignore the returned error as we cannot do anything about it anyway
}
// listen creates a http server on the zipkin instance it is called with, and
// serves http until it is stopped by Zipkin's (*Zipkin).Stop() method.
func (z *Zipkin) listen(ln net.Listener, acc telegraf.Accumulator) {
if err := z.server.Serve(ln); err != nil {
// Because of the clean shutdown in `(*Zipkin).Stop()`
// We're expecting a server closed error at some point
// So we don't want to display it as an error.
// This interferes with telegraf's internal data collection,
// by making it appear as if a serious error occurred.
if err != http.ErrServerClosed {
acc.AddError(fmt.Errorf("error listening: %w", err))
}
}
}
func init() {
inputs.Add("zipkin", func() telegraf.Input {
return &Zipkin{
Path: defaultRoute,
Port: defaultPort,
}
})
}

View file

@ -0,0 +1,662 @@
package zipkin
import (
"bytes"
"fmt"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/testutil"
)
func TestZipkinPlugin(t *testing.T) {
mockAcc := testutil.Accumulator{}
tests := []struct {
name string
datafile string // data file which contains test data
contentType string
wantErr bool
want []testutil.Metric
}{
{
name: "threespan",
datafile: "testdata/threespans.dat",
contentType: "application/x-thrift",
want: []testutil.Metric{
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "7047c59776af8a1b",
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"service_name": "trivial",
"name": "child",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(53106) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851331000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "7047c59776af8a1b",
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"name": "child",
"service_name": "trivial",
"annotation": "trivial", // base64: dHJpdmlhbA==
"endpoint_host": "127.0.0.1",
"annotation_key": "lc",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(53106) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851331000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "17020eb55a8bfe5",
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"service_name": "trivial",
"name": "child",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(50410) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360904552000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "17020eb55a8bfe5",
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"name": "child",
"service_name": "trivial",
"annotation": "trivial", // base64: dHJpdmlhbA==
"endpoint_host": "127.0.0.1",
"annotation_key": "lc",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(50410) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360904552000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "5195e96239641e",
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"service_name": "trivial",
"name": "parent",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"service_name": "trivial",
"annotation": "Starting child #0",
"endpoint_host": "127.0.0.1",
"id": "5195e96239641e",
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"name": "parent",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"service_name": "trivial",
"annotation": "Starting child #1",
"endpoint_host": "127.0.0.1",
"id": "5195e96239641e",
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"name": "parent",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"parent_id": "5195e96239641e",
"trace_id": "22c4fc8ab3669045",
"name": "parent",
"service_name": "trivial",
"annotation": "A Log",
"endpoint_host": "127.0.0.1",
"id": "5195e96239641e",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"trace_id": "22c4fc8ab3669045",
"service_name": "trivial",
"annotation": "trivial", // base64: dHJpdmlhbA==
"annotation_key": "lc",
"id": "5195e96239641e",
"parent_id": "5195e96239641e",
"name": "parent",
"endpoint_host": "127.0.0.1",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(103680) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1498688360851318000).UTC(),
Type: telegraf.Untyped,
},
},
wantErr: false,
},
{
name: "distributed_trace_sample",
datafile: "testdata/distributed_trace_sample.dat",
contentType: "application/x-thrift",
want: []testutil.Metric{
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "5e682bc21ce99c80",
"parent_id": "5e682bc21ce99c80",
"trace_id": "5e682bc21ce99c80",
"service_name": "go-zipkin-testclient",
"name": "main.dud",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(1) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1433330263415871*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "cs",
"endpoint_host": "0.0.0.0:9410",
"id": "5e682bc21ce99c80",
"parent_id": "5e682bc21ce99c80",
"trace_id": "5e682bc21ce99c80",
"name": "main.dud",
"service_name": "go-zipkin-testclient",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(1) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1433330263415871*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "cr",
"endpoint_host": "0.0.0.0:9410",
"id": "5e682bc21ce99c80",
"parent_id": "5e682bc21ce99c80",
"trace_id": "5e682bc21ce99c80",
"name": "main.dud",
"service_name": "go-zipkin-testclient",
},
Fields: map[string]interface{}{
"duration_ns": (time.Duration(1) * time.Microsecond).Nanoseconds(),
},
Time: time.Unix(0, 1433330263415871*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
},
},
{
name: "JSON rather than thrift",
datafile: "testdata/json/brave-tracer-example.json",
contentType: "application/json",
want: []testutil.Metric{
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(3000000),
},
Time: time.Unix(0, 1503031538791000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "sr",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(3000000),
},
Time: time.Unix(0, 1503031538791000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "ss",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(3000000),
},
Time: time.Unix(0, 1503031538791000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "Demo2Application",
"annotation_key": "mvc.controller.class",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(3000000),
},
Time: time.Unix(0, 1503031538791000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "hi2",
"annotation_key": "mvc.controller.method",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(3000000),
},
Time: time.Unix(0, 1503031538791000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "192.168.0.8:test:8010",
"annotation_key": "spring.instance_id",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(3000000),
},
Time: time.Unix(0, 1503031538791000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "cs",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "cr",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "localhost",
"annotation_key": "http.host",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "GET",
"annotation_key": "http.method",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "/hi2",
"annotation_key": "http.path",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "http://localhost:8010/hi2",
"annotation_key": "http.url",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "192.168.0.8:test:8010",
"annotation_key": "spring.instance_id",
"endpoint_host": "192.168.0.8:8010",
"id": "b26412d1ac16767d",
"name": "http:/hi2",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(10000000),
},
Time: time.Unix(0, 1503031538786000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"id": "7312f822d43d0fd8",
"name": "http:/hi",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(23393000),
},
Time: time.Unix(0, 1503031538778000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "sr",
"endpoint_host": "192.168.0.8:8010",
"id": "7312f822d43d0fd8",
"name": "http:/hi",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(23393000),
},
Time: time.Unix(0, 1503031538778000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "ss",
"endpoint_host": "192.168.0.8:8010",
"id": "7312f822d43d0fd8",
"name": "http:/hi",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(23393000),
},
Time: time.Unix(0, 1503031538778000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "Demo2Application",
"annotation_key": "mvc.controller.class",
"endpoint_host": "192.168.0.8:8010",
"id": "7312f822d43d0fd8",
"name": "http:/hi",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(23393000),
},
Time: time.Unix(0, 1503031538778000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "hi",
"annotation_key": "mvc.controller.method",
"endpoint_host": "192.168.0.8:8010",
"id": "7312f822d43d0fd8",
"name": "http:/hi",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(23393000),
},
Time: time.Unix(0, 1503031538778000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
{
Measurement: "zipkin",
Tags: map[string]string{
"annotation": "192.168.0.8:test:8010",
"annotation_key": "spring.instance_id",
"endpoint_host": "192.168.0.8:8010",
"id": "7312f822d43d0fd8",
"name": "http:/hi",
"parent_id": "7312f822d43d0fd8",
"service_name": "test",
"trace_id": "7312f822d43d0fd8",
},
Fields: map[string]interface{}{
"duration_ns": int64(23393000),
},
Time: time.Unix(0, 1503031538778000*int64(time.Microsecond)).UTC(),
Type: telegraf.Untyped,
},
},
},
}
// Workaround for Go 1.8
// https://github.com/golang/go/issues/18806
defaultNetwork = "tcp4"
z := &Zipkin{
Log: testutil.Logger{},
Path: "/api/v1/spans",
Port: 0,
}
err := z.Start(&mockAcc)
if err != nil {
t.Fatal("Failed to start zipkin server")
}
defer z.Stop()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockAcc.ClearMetrics()
if err := postThriftData(tt.datafile, z.address, tt.contentType); err != nil {
t.Fatalf("Posting data to http endpoint /api/v1/spans failed. Error: %s\n", err)
}
mockAcc.Wait(
len(tt.want),
) // Since the server is running concurrently, we need to wait for the number of data points we want to test to be added to the Accumulator.
if len(mockAcc.Errors) > 0 != tt.wantErr {
t.Fatalf("Got unexpected errors. want error = %v, errors = %v\n", tt.wantErr, mockAcc.Errors)
}
var got []testutil.Metric
for _, m := range mockAcc.Metrics {
got = append(got, *m)
}
require.Equal(t, tt.want, got)
})
}
mockAcc.ClearMetrics()
z.Stop()
// Make sure there is no erroneous error on shutdown
if len(mockAcc.Errors) != 0 {
t.Fatal("Expected no errors on shutdown")
}
}
func postThriftData(datafile, address, contentType string) error {
dat, err := os.ReadFile(datafile)
if err != nil {
return fmt.Errorf("could not read from data file %s", datafile)
}
endpoint := fmt.Sprintf("http://%s/api/v1/spans", address)
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(dat))
if err != nil {
return fmt.Errorf("unable to create new POST request for %q: %w", endpoint, err)
}
req.Header.Set("Content-Type", contentType)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error while making HTTP POST request to zipkin endpoint %q: %w", endpoint, err)
}
defer resp.Body.Close()
return nil
}