diff --git a/Makefile b/Makefile index 636dabe18eb..5a704eb26d5 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ PROJECT_ROOT=github.com/jaegertracing/jaeger -TOP_PKGS := $(shell glide novendor | grep -v -e ./thrift-gen/... -e ./examples/... -e ./scripts/...) +TOP_PKGS := $(shell glide novendor | grep -v -e ./thrift-gen/... -e swagger-gen... -e ./examples/... -e ./scripts/...) # all .go files that don't exist in hidden directories -ALL_SRC := $(shell find . -name "*.go" | grep -v -e vendor -e thrift-gen \ +ALL_SRC := $(shell find . -name "*.go" | grep -v -e vendor -e thrift-gen -e swagger-gen \ -e ".*/\..*" \ -e ".*/_.*" \ -e ".*/mocks.*") @@ -34,6 +34,11 @@ THRIFT_GO_ARGS=thrift_import="github.com/apache/thrift/lib/go/thrift" THRIFT_GEN=$(shell which thrift-gen) THRIFT_GEN_DIR=thrift-gen +SWAGGER_VER=0.12.0 +SWAGGER_IMAGE=quay.io/goswagger/swagger:$(SWAGGER_VER) +SWAGGER=docker run --rm -it -u ${shell id -u} -v "${PWD}:/go/src/${PROJECT_ROOT}" -w /go/src/${PROJECT_ROOT} $(SWAGGER_IMAGE) +SWAGGER_GEN_DIR=swagger-gen + PASS=$(shell printf "\033[32mPASS\033[0m") FAIL=$(shell printf "\033[31mFAIL\033[0m") COLORIZE=$(SED) ''/PASS/s//$(PASS)/'' | $(SED) ''/FAIL/s//$(FAIL)/'' @@ -219,6 +224,11 @@ idl-submodule: thrift-image: $(THRIFT) -version +.PHONY: generate-zipkin-swagger +generate-zipkin-swagger: idl-submodule + $(SWAGGER) generate server -f ./idl/swagger/zipkin2-api.yaml -t $(SWAGGER_GEN_DIR) -O PostSpans --exclude-main + rm $(SWAGGER_GEN_DIR)/restapi/operations/post_spans_urlbuilder.go $(SWAGGER_GEN_DIR)/restapi/server.go $(SWAGGER_GEN_DIR)/restapi/configure_zipkin.go $(SWAGGER_GEN_DIR)/models/trace.go $(SWAGGER_GEN_DIR)/models/list_of_traces.go $(SWAGGER_GEN_DIR)/models/dependency_link.go + .PHONY: docs docs: $(MKDOCS_VIRTUAL_ENV) bash -c 'source $(MKDOCS_VIRTUAL_ENV)/bin/activate; mkdocs serve' diff --git a/cmd/collector/app/zipkin/fixtures/zipkin_01.json b/cmd/collector/app/zipkin/fixtures/zipkin_01.json new file mode 100644 index 00000000000..69f874d776d --- /dev/null +++ b/cmd/collector/app/zipkin/fixtures/zipkin_01.json @@ -0,0 +1,28 @@ +[ + { + "traceId":"1", + "id":"2", + "parentId": "1", + "name":"foo", + "kind": "CLIENT", + "debug": true, + "shared": true, + "timestamp": 1, + "duration": 10, + "localEndpoint":{ + "serviceName":"foo", + "ipv4":"10.43.17.42" + }, + "remoteEndpoint":{ + "serviceName":"bar", + "ipv4":"10.43.17.43" + }, + "annotations": [{ + "value": "foo", + "timestamp": 1 + }], + "tags": { + "foo": "bar" + } + } +] diff --git a/cmd/collector/app/zipkin/http_handler.go b/cmd/collector/app/zipkin/http_handler.go index 10f5410c11b..88efb286da7 100644 --- a/cmd/collector/app/zipkin/http_handler.go +++ b/cmd/collector/app/zipkin/http_handler.go @@ -17,44 +17,54 @@ package zipkin import ( "compress/gzip" "fmt" + "io" "io/ioutil" "net/http" "strings" "time" "github.com/apache/thrift/lib/go/thrift" + "github.com/go-openapi/loads" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/gorilla/mux" tchanThrift "github.com/uber/tchannel-go/thrift" "github.com/jaegertracing/jaeger/cmd/collector/app" + "github.com/jaegertracing/jaeger/swagger-gen/models" + "github.com/jaegertracing/jaeger/swagger-gen/restapi" + "github.com/jaegertracing/jaeger/swagger-gen/restapi/operations" "github.com/jaegertracing/jaeger/thrift-gen/zipkincore" ) // APIHandler handles all HTTP calls to the collector type APIHandler struct { zipkinSpansHandler app.ZipkinSpansHandler + zipkinV2Formats strfmt.Registry } // NewAPIHandler returns a new APIHandler func NewAPIHandler( zipkinSpansHandler app.ZipkinSpansHandler, ) *APIHandler { + swaggerSpec, _ := loads.Analyzed(restapi.SwaggerJSON, "") return &APIHandler{ zipkinSpansHandler: zipkinSpansHandler, + zipkinV2Formats: operations.NewZipkinAPI(swaggerSpec).Formats(), } } // RegisterRoutes registers Zipkin routes func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/spans", aH.saveSpans).Methods(http.MethodPost) + router.HandleFunc("/api/v2/spans", aH.saveSpansV2).Methods(http.MethodPost) } func (aH *APIHandler) saveSpans(w http.ResponseWriter, r *http.Request) { bRead := r.Body defer r.Body.Close() - if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") { - gz, err := gzip.NewReader(r.Body) + gz, err := gunzip(bRead) if err != nil { http.Error(w, fmt.Sprintf(app.UnableToReadBodyErrFormat, err), http.StatusBadRequest) return @@ -84,15 +94,79 @@ func (aH *APIHandler) saveSpans(w http.ResponseWriter, r *http.Request) { return } - if len(tSpans) > 0 { - ctx, _ := tchanThrift.NewContext(time.Minute) - if _, err = aH.zipkinSpansHandler.SubmitZipkinBatch(ctx, tSpans); err != nil { - http.Error(w, fmt.Sprintf("Cannot submit Zipkin batch: %v", err), http.StatusInternalServerError) + if err := aH.saveThriftSpans(tSpans); err != nil { + http.Error(w, fmt.Sprintf("Cannot submit Zipkin batch: %v", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusAccepted) +} + +func (aH *APIHandler) saveSpansV2(w http.ResponseWriter, r *http.Request) { + bRead := r.Body + defer r.Body.Close() + if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") { + gz, err := gunzip(bRead) + if err != nil { + http.Error(w, fmt.Sprintf(app.UnableToReadBodyErrFormat, err), http.StatusBadRequest) return } + defer gz.Close() + bRead = gz } - w.WriteHeader(http.StatusAccepted) + bodyBytes, err := ioutil.ReadAll(bRead) + if err != nil { + http.Error(w, fmt.Sprintf(app.UnableToReadBodyErrFormat, err), http.StatusInternalServerError) + return + } + + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + http.Error(w, "Unsupported Content-Type", http.StatusBadRequest) + return + } + + var spans models.ListOfSpans + if err = swag.ReadJSON(bodyBytes, &spans); err != nil { + http.Error(w, fmt.Sprintf(app.UnableToReadBodyErrFormat, err), http.StatusBadRequest) + return + } + if err = spans.Validate(aH.zipkinV2Formats); err != nil { + http.Error(w, fmt.Sprintf(app.UnableToReadBodyErrFormat, err), http.StatusBadRequest) + return + } + + tSpans, err := spansV2ToThrift(spans) + if err != nil { + http.Error(w, fmt.Sprintf(app.UnableToReadBodyErrFormat, err), http.StatusBadRequest) + return + } + + if err := aH.saveThriftSpans(tSpans); err != nil { + http.Error(w, fmt.Sprintf("Cannot submit Zipkin batch: %v", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(operations.PostSpansAcceptedCode) +} + +func gunzip(r io.ReadCloser) (*gzip.Reader, error) { + gz, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + return gz, nil +} + +func (aH *APIHandler) saveThriftSpans(tSpans []*zipkincore.Span) error { + if len(tSpans) > 0 { + ctx, _ := tchanThrift.NewContext(time.Minute) + if _, err := aH.zipkinSpansHandler.SubmitZipkinBatch(ctx, tSpans); err != nil { + return err + } + } + return nil } func deserializeThrift(b []byte) ([]*zipkincore.Span, error) { diff --git a/cmd/collector/app/zipkin/http_handler_test.go b/cmd/collector/app/zipkin/http_handler_test.go index f367bb3c5b8..fee8f4d9ffe 100644 --- a/cmd/collector/app/zipkin/http_handler_test.go +++ b/cmd/collector/app/zipkin/http_handler_test.go @@ -103,20 +103,13 @@ func waitForSpans(t *testing.T, handler *mockZipkinHandler, expecting int) { } func TestThriftFormat(t *testing.T) { - server, handler := initializeTestServer(nil) + server, _ := initializeTestServer(nil) defer server.Close() - bodyBytes := zipkinSerialize([]*zipkincore.Span{{}}) statusCode, resBodyStr, err := postBytes(server.URL+`/api/v1/spans`, bodyBytes, createHeader("application/x-thrift")) assert.NoError(t, err) assert.EqualValues(t, http.StatusAccepted, statusCode) assert.EqualValues(t, "", resBodyStr) - - handler.zipkinSpansHandler.(*mockZipkinHandler).err = fmt.Errorf("Bad times ahead") - statusCode, resBodyStr, err = postBytes(server.URL+`/api/v1/spans`, bodyBytes, createHeader("application/x-thrift")) - assert.NoError(t, err) - assert.EqualValues(t, http.StatusInternalServerError, statusCode) - assert.EqualValues(t, "Cannot submit Zipkin batch: Bad times ahead\n", resBodyStr) } func TestJsonFormat(t *testing.T) { @@ -228,9 +221,52 @@ func TestCannotReadBodyFromRequest(t *testing.T) { req, err := http.NewRequest(http.MethodPost, "whatever", &errReader{}) assert.NoError(t, err) rw := dummyResponseWriter{} - handler.saveSpans(&rw, req) - assert.EqualValues(t, http.StatusInternalServerError, rw.myStatusCode) - assert.EqualValues(t, "Unable to process request body: Simulated error reading body\n", rw.myBody) + + tests := []struct { + handler func(w http.ResponseWriter, r *http.Request) + }{ + {handler: handler.saveSpans}, + {handler: handler.saveSpansV2}, + } + for _, test := range tests { + test.handler(&rw, req) + assert.EqualValues(t, http.StatusInternalServerError, rw.myStatusCode) + assert.EqualValues(t, "Unable to process request body: Simulated error reading body\n", rw.myBody) + } +} + +func TestSaveSpansV2(t *testing.T) { + server, handler := initializeTestServer(nil) + defer server.Close() + tests := []struct { + body []byte + resBody string + code int + headers map[string]string + }{ + {body: []byte("[]"), code: http.StatusAccepted}, + {body: gzipEncode([]byte("[]")), code: http.StatusAccepted, headers: map[string]string{"Content-Encoding": "gzip"}}, + {body: []byte("[]"), code: http.StatusBadRequest, headers: map[string]string{"Content-Type": "text/html"}, resBody: "Unsupported Content-Type\n"}, + {body: []byte("[]"), code: http.StatusBadRequest, headers: map[string]string{"Content-Encoding": "gzip"}, resBody: "Unable to process request body: unexpected EOF\n"}, + {body: []byte("not good"), code: http.StatusBadRequest, resBody: "Unable to process request body: invalid character 'o' in literal null (expecting 'u')\n"}, + {body: []byte("[{}]"), code: http.StatusBadRequest, resBody: "Unable to process request body: validation failure list:\nid in body is required\ntraceId in body is required\n"}, + {body: []byte(`[{"id":"1111111111111111", "traceId":"1111111111111111", "localEndpoint": {"ipv4": "A"}}]`), code: http.StatusBadRequest, resBody: "Unable to process request body: wrong ipv4\n"}, + } + for _, test := range tests { + h := createHeader("application/json") + for k, v := range test.headers { + h.Set(k, v) + } + statusCode, resBody, err := postBytes(server.URL+`/api/v2/spans`, test.body, h) + require.NoError(t, err) + assert.EqualValues(t, test.code, statusCode) + assert.EqualValues(t, test.resBody, resBody) + } + handler.zipkinSpansHandler.(*mockZipkinHandler).err = fmt.Errorf("Bad times ahead") + statusCode, resBody, err := postBytes(server.URL+`/api/v2/spans`, []byte(`[{"id":"1111111111111111", "traceId":"1111111111111111"}]`), createHeader("application/json")) + require.NoError(t, err) + assert.EqualValues(t, http.StatusInternalServerError, statusCode) + assert.EqualValues(t, "Cannot submit Zipkin batch: Bad times ahead\n", resBody) } type errReader struct{} diff --git a/cmd/collector/app/zipkin/json.go b/cmd/collector/app/zipkin/json.go index d27fefbd4fd..d54747b05b5 100644 --- a/cmd/collector/app/zipkin/json.go +++ b/cmd/collector/app/zipkin/json.go @@ -153,29 +153,35 @@ func cutLongID(id string) string { } func endpointToThrift(e endpoint) (*zipkincore.Endpoint, error) { - ipv4, err := parseIpv4(e.IPv4) + return eToThrift(e.IPv4, e.IPv6, e.Port, e.ServiceName) +} + +func eToThrift(ip4 string, ip6 string, p int32, service string) (*zipkincore.Endpoint, error) { + ipv4, err := parseIpv4(ip4) if err != nil { return nil, err } - port := e.Port - if port >= (1 << 15) { - // Zipkin.thrift defines port as i16, so values between (2^15 and 2^16-1) must be encoded as negative - port = port - (1 << 16) - } - - ipv6, err := parseIpv6(e.IPv6) + port := port(p) + ipv6, err := parseIpv6(string(ip6)) if err != nil { return nil, err } - return &zipkincore.Endpoint{ - ServiceName: e.ServiceName, + ServiceName: service, Port: int16(port), Ipv4: ipv4, Ipv6: ipv6, }, nil } +func port(p int32) int32 { + if p >= (1 << 15) { + // Zipkin.thrift defines port as i16, so values between (2^15 and 2^16-1) must be encoded as negative + p = p - (1 << 16) + } + return p +} + func annoToThrift(a annotation) (*zipkincore.Annotation, error) { endpoint, err := endpointToThrift(a.Endpoint) if err != nil { diff --git a/cmd/collector/app/zipkin/jsonv2.go b/cmd/collector/app/zipkin/jsonv2.go new file mode 100644 index 00000000000..1d9c204756d --- /dev/null +++ b/cmd/collector/app/zipkin/jsonv2.go @@ -0,0 +1,178 @@ +// Copyright (c) 2017 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zipkin + +import ( + "github.com/jaegertracing/jaeger/model" + "github.com/jaegertracing/jaeger/swagger-gen/models" + "github.com/jaegertracing/jaeger/thrift-gen/zipkincore" +) + +func spansV2ToThrift(spans models.ListOfSpans) ([]*zipkincore.Span, error) { + tSpans := make([]*zipkincore.Span, 0, len(spans)) + for _, span := range spans { + tSpan, err := spanV2ToThrift(span) + if err != nil { + return nil, err + } + tSpans = append(tSpans, tSpan) + } + return tSpans, nil +} + +func spanV2ToThrift(s *models.Span) (*zipkincore.Span, error) { + id, err := model.SpanIDFromString(cutLongID(*s.ID)) + if err != nil { + return nil, err + } + traceID, err := model.TraceIDFromString(*s.TraceID) + if err != nil { + return nil, err + } + tSpan := &zipkincore.Span{ + ID: int64(id), + TraceID: int64(traceID.Low), + Name: s.Name, + Debug: s.Debug, + Timestamp: &s.Timestamp, + Duration: &s.Duration, + } + + if len(s.ParentID) > 0 { + parentID, err := model.SpanIDFromString(cutLongID(s.ParentID)) + if err != nil { + return nil, err + } + signed := int64(parentID) + tSpan.ParentID = &signed + } + + var localE *zipkincore.Endpoint + if s.LocalEndpoint != nil { + localE, err = endpointV2ToThrift(s.LocalEndpoint) + if err != nil { + return nil, err + } + } + + for _, a := range s.Annotations { + tA := annoV2ToThrift(a, localE) + tSpan.Annotations = append(tSpan.Annotations, tA) + } + + tSpan.BinaryAnnotations = append(tSpan.BinaryAnnotations, tagsToThrift(s.Tags, localE)...) + tSpan.Annotations = append(tSpan.Annotations, kindToThrift(s.Timestamp, s.Duration, s.Kind, localE)...) + + if s.RemoteEndpoint != nil { + rAddrAnno, err := remoteEndpToThrift(s.RemoteEndpoint, s.Kind) + if err != nil { + return nil, err + } + tSpan.BinaryAnnotations = append(tSpan.BinaryAnnotations, rAddrAnno) + } + return tSpan, nil +} + +func remoteEndpToThrift(e *models.Endpoint, kind string) (*zipkincore.BinaryAnnotation, error) { + rEndp, err := endpointV2ToThrift(e) + if err != nil { + return nil, err + } + var key string + switch kind { + case models.SpanKindCLIENT: + key = zipkincore.SERVER_ADDR + case models.SpanKindSERVER: + key = zipkincore.CLIENT_ADDR + case models.SpanKindCONSUMER, models.SpanKindPRODUCER: + key = zipkincore.MESSAGE_ADDR + } + + return &zipkincore.BinaryAnnotation{ + Key: key, + Host: rEndp, + AnnotationType: zipkincore.AnnotationType_BOOL, + }, nil +} + +func kindToThrift(ts int64, d int64, kind string, localE *zipkincore.Endpoint) []*zipkincore.Annotation { + var annos []*zipkincore.Annotation + switch kind { + case models.SpanKindSERVER: + annos = append(annos, &zipkincore.Annotation{ + Value: zipkincore.SERVER_RECV, + Host: localE, + Timestamp: ts, + }) + annos = append(annos, &zipkincore.Annotation{ + Value: zipkincore.SERVER_SEND, + Host: localE, + Timestamp: ts + d, + }) + case models.SpanKindCLIENT: + annos = append(annos, &zipkincore.Annotation{ + Value: zipkincore.CLIENT_SEND, + Host: localE, + Timestamp: ts, + }) + annos = append(annos, &zipkincore.Annotation{ + Value: zipkincore.CLIENT_RECV, + Host: localE, + Timestamp: ts + d, + }) + case models.SpanKindPRODUCER: + annos = append(annos, &zipkincore.Annotation{ + Value: zipkincore.MESSAGE_SEND, + Host: localE, + Timestamp: ts, + }) + case models.SpanKindCONSUMER: + annos = append(annos, &zipkincore.Annotation{ + Value: zipkincore.MESSAGE_RECV, + Host: localE, + Timestamp: ts, + }) + } + return annos +} + +func endpointV2ToThrift(e *models.Endpoint) (*zipkincore.Endpoint, error) { + if e == nil { + return nil, nil + } + return eToThrift(string(e.IPV4), string(e.IPV6), int32(e.Port), e.ServiceName) +} + +func annoV2ToThrift(a *models.Annotation, e *zipkincore.Endpoint) *zipkincore.Annotation { + return &zipkincore.Annotation{ + Value: a.Value, + Timestamp: a.Timestamp, + Host: e, + } +} + +func tagsToThrift(tags models.Tags, localE *zipkincore.Endpoint) []*zipkincore.BinaryAnnotation { + bAnnos := make([]*zipkincore.BinaryAnnotation, 0, len(tags)) + for k, v := range tags { + ba := &zipkincore.BinaryAnnotation{ + Key: k, + Value: []byte(v), + AnnotationType: zipkincore.AnnotationType_STRING, + Host: localE, + } + bAnnos = append(bAnnos, ba) + } + return bAnnos +} diff --git a/cmd/collector/app/zipkin/jsonv2_test.go b/cmd/collector/app/zipkin/jsonv2_test.go new file mode 100644 index 00000000000..0272614e5be --- /dev/null +++ b/cmd/collector/app/zipkin/jsonv2_test.go @@ -0,0 +1,135 @@ +// Copyright (c) 2017 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zipkin + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/go-openapi/swag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jaegertracing/jaeger/swagger-gen/models" + "github.com/jaegertracing/jaeger/thrift-gen/zipkincore" +) + +func TestFixtures(t *testing.T) { + var spans models.ListOfSpans + loadJSON(t, fmt.Sprintf("fixtures/zipkin_01.json"), &spans) + tSpans, err := spansV2ToThrift(spans) + require.NoError(t, err) + assert.Equal(t, len(tSpans), 1) + var pid int64 = 1 + var ts int64 = 1 + var d int64 = 10 + localE := &zipkincore.Endpoint{ServiceName: "foo", Ipv4: 170594602} + remoteE := &zipkincore.Endpoint{ServiceName: "bar", Ipv4: 170594603} + tSpan := &zipkincore.Span{ID: 2, TraceID: 1, ParentID: &pid, Name: "foo", Debug: true, Duration: &d, Timestamp: &ts, + Annotations: []*zipkincore.Annotation{ + {Value: "foo", Timestamp: 1, Host: localE}, + {Value: zipkincore.CLIENT_SEND, Timestamp: ts, Host: localE}, + {Value: zipkincore.CLIENT_RECV, Timestamp: ts + d, Host: localE}}, + BinaryAnnotations: []*zipkincore.BinaryAnnotation{ + {Key: "foo", Value: []byte("bar"), Host: localE, AnnotationType: zipkincore.AnnotationType_STRING}, + {Key: zipkincore.SERVER_ADDR, Host: remoteE, AnnotationType: zipkincore.AnnotationType_BOOL}}} + assert.Equal(t, tSpans[0], tSpan) +} + +func TestKindToThrift(t *testing.T) { + tests := []struct { + ts int64 + d int64 + kind string + expected []*zipkincore.Annotation + }{ + {kind: models.SpanKindCLIENT, ts: 0, d: 1, expected: []*zipkincore.Annotation{{Value: zipkincore.CLIENT_SEND, Timestamp: 0}, {Value: zipkincore.CLIENT_RECV, Timestamp: 1}}}, + {kind: models.SpanKindSERVER, ts: 0, d: 1, expected: []*zipkincore.Annotation{{Value: zipkincore.SERVER_RECV, Timestamp: 0}, {Value: zipkincore.SERVER_SEND, Timestamp: 1}}}, + {kind: models.SpanKindPRODUCER, ts: 0, d: 1, expected: []*zipkincore.Annotation{{Value: zipkincore.MESSAGE_SEND, Timestamp: 0}}}, + {kind: models.SpanKindCONSUMER, ts: 0, d: 1, expected: []*zipkincore.Annotation{{Value: zipkincore.MESSAGE_RECV, Timestamp: 0}}}, + } + for _, test := range tests { + banns := kindToThrift(test.ts, test.d, test.kind, nil) + assert.Equal(t, banns, test.expected) + } +} + +func TestRemoteEndpToThrift(t *testing.T) { + tests := []struct { + kind string + expected *zipkincore.BinaryAnnotation + }{ + {kind: models.SpanKindCLIENT, expected: &zipkincore.BinaryAnnotation{Key: zipkincore.SERVER_ADDR, AnnotationType: zipkincore.AnnotationType_BOOL}}, + {kind: models.SpanKindSERVER, expected: &zipkincore.BinaryAnnotation{Key: zipkincore.CLIENT_ADDR, AnnotationType: zipkincore.AnnotationType_BOOL}}, + {kind: models.SpanKindPRODUCER, expected: &zipkincore.BinaryAnnotation{Key: zipkincore.MESSAGE_ADDR, AnnotationType: zipkincore.AnnotationType_BOOL}}, + {kind: models.SpanKindCONSUMER, expected: &zipkincore.BinaryAnnotation{Key: zipkincore.MESSAGE_ADDR, AnnotationType: zipkincore.AnnotationType_BOOL}}, + } + for _, test := range tests { + banns, err := remoteEndpToThrift(nil, test.kind) + require.NoError(t, err) + assert.Equal(t, banns, test.expected) + } +} + +func TestErrIds(t *testing.T) { + idOk := "a" + idWrong := "z" + tests := []struct { + span models.Span + }{ + {span: models.Span{ID: &idWrong}}, + {span: models.Span{ID: &idOk, TraceID: &idWrong}}, + {span: models.Span{ID: &idOk, TraceID: &idOk, ParentID: idWrong}}, + } + for _, test := range tests { + tSpan, err := spanV2ToThrift(&test.span) + require.Error(t, err) + require.Nil(t, tSpan) + assert.Equal(t, err.Error(), "strconv.ParseUint: parsing \"z\": invalid syntax") + } +} + +func TestErrEndpoints(t *testing.T) { + id := "A" + endp := models.Endpoint{IPV4: "192.168.0.0.1"} + tests := []struct { + span models.Span + }{ + {span: models.Span{ID: &id, TraceID: &id, LocalEndpoint: &endp}}, + {span: models.Span{ID: &id, TraceID: &id, RemoteEndpoint: &endp}}, + } + for _, test := range tests { + tSpan, err := spanV2ToThrift(&test.span) + require.Error(t, err) + require.Nil(t, tSpan) + assert.Equal(t, err.Error(), "wrong ipv4") + } +} + +func TestErrSpans(t *testing.T) { + id := "z" + tSpans, err := spansV2ToThrift(models.ListOfSpans{&models.Span{ID: &id}}) + require.Error(t, err) + require.Nil(t, tSpans) + assert.Equal(t, err.Error(), "strconv.ParseUint: parsing \"z\": invalid syntax") +} + +func loadJSON(t *testing.T, fileName string, i interface{}) { + b, err := ioutil.ReadFile(fileName) + require.NoError(t, err) + err = swag.ReadJSON(b, i) + require.NoError(t, err, "Failed to parse json fixture file %s", fileName) +} diff --git a/cmd/collector/main.go b/cmd/collector/main.go index f9011d7e75b..10f478fc79b 100644 --- a/cmd/collector/main.go +++ b/cmd/collector/main.go @@ -164,8 +164,10 @@ func startZipkinHTTPAPI( recoveryHandler func(http.Handler) http.Handler, ) { if zipkinPort != 0 { + zHandler := zipkin.NewAPIHandler(zipkinSpansHandler) r := mux.NewRouter() - zipkin.NewAPIHandler(zipkinSpansHandler).RegisterRoutes(r) + zHandler.RegisterRoutes(r) + httpPortStr := ":" + strconv.Itoa(zipkinPort) logger.Info("Listening for Zipkin HTTP traffic", zap.Int("zipkin.http-port", zipkinPort)) diff --git a/cmd/standalone/main.go b/cmd/standalone/main.go index c125130476f..e7ae727c494 100644 --- a/cmd/standalone/main.go +++ b/cmd/standalone/main.go @@ -186,7 +186,8 @@ func startZipkinHTTPAPI( ) { if zipkinPort != 0 { r := mux.NewRouter() - zipkin.NewAPIHandler(zipkinSpansHandler).RegisterRoutes(r) + zHandler := zipkin.NewAPIHandler(zipkinSpansHandler) + zHandler.RegisterRoutes(r) httpPortStr := ":" + strconv.Itoa(zipkinPort) logger.Info("Listening for Zipkin HTTP traffic", zap.Int("zipkin.http-port", zipkinPort)) diff --git a/crossdock/docker-compose.yml b/crossdock/docker-compose.yml index 48a3047d77e..0b5053f4857 100644 --- a/crossdock/docker-compose.yml +++ b/crossdock/docker-compose.yml @@ -10,16 +10,17 @@ services: - java - python - zipkin-brave-json + - zipkin-brave-json-v2 - zipkin-brave-thrift environment: - - WAIT_FOR=test_driver,go,node,java,python,zipkin-brave-thrift,zipkin-brave-json + - WAIT_FOR=test_driver,go,node,java,python,zipkin-brave-thrift,zipkin-brave-json,zipkin-brave-json-v2 - WAIT_FOR_TIMEOUT=60s - CALL_TIMEOUT=60s - AXIS_CLIENT=test_driver - - AXIS_SERVICES=go,node,java,python,zipkin-brave-json,zipkin-brave-thrift + - AXIS_SERVICES=go,node,java,python,zipkin-brave-json,zipkin-brave-json-v2,zipkin-brave-thrift - BEHAVIOR_ENDTOEND=client,services @@ -53,6 +54,14 @@ services: environment: - ENCODING=JSON + zipkin-brave-json-v2: + image: jaegertracing/xdock-zipkin-brave + ports: + - "8080-8081" + environment: + - ENCODING=JSON + - JSON_ENCODER=JSON_V2 + zipkin-brave-thrift: image: jaegertracing/xdock-zipkin-brave ports: diff --git a/crossdock/services/query.go b/crossdock/services/query.go index 30fc41eff12..64c20a00afa 100644 --- a/crossdock/services/query.go +++ b/crossdock/services/query.go @@ -64,13 +64,14 @@ func (s *queryService) GetTraces(serviceName, operation string, tags map[string] for k, v := range tags { values.Add("tag", k+":"+v) } - resp, err := http.Get(fmt.Sprintf(getTraceURL(s.url), values.Encode())) + url := fmt.Sprintf(getTraceURL(s.url), values.Encode()) + resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) - s.logger.Info("Retrieved trace from query", zap.String("body", string(body))) + s.logger.Info("Retrieved trace from query", zap.String("body", string(body)), zap.String("url", url)) var queryResponse response if err = json.Unmarshal(body, &queryResponse); err != nil { diff --git a/docs/getting_started.md b/docs/getting_started.md index 82457d64d78..bd229116cc6 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -73,7 +73,7 @@ Then navigate to `http://localhost:8080`. #### Prerequisites -- You need Go 1.7 or higher installed on your machine. +- You need Go 1.9 or higher installed on your machine. - Requires a [running Jaeger backend](#all-in-one-docker-image) to view the traces. ## Client Libraries diff --git a/docs/index.md b/docs/index.md index 2f9d261729b..bd664a61fb8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ for the architectural choices made in Jaeger. ## Technical Specs - * Backend components implemented in Go + * Backend components implemented in Go 1.9 * React/Javascript UI * [Cassandra 3.x](https://github.com/jaegertracing/jaeger/tree/master/plugin/storage/cassandra), [ElasticSearch](https://github.com/jaegertracing/jaeger/tree/master/plugin/storage/es) as persistent storage (more storage backends coming soon) diff --git a/glide.lock b/glide.lock index 505025d8aeb..f46f106b8f4 100644 --- a/glide.lock +++ b/glide.lock @@ -1,10 +1,12 @@ -hash: 186c8ac164ff232e786b5be66349990e0a87c188de41b193fc6a279d413c0a71 -updated: 2017-10-17T15:33:35.040726046-04:00 +hash: 2209e73de25d42fa6634f92b2126e8b1b57d3f910d67c574afd4a83168d07009 +updated: 2017-11-08T17:37:34.445195465+01:00 imports: - name: github.com/apache/thrift version: 53dd39833a08ce33582e5ff31fa18bb4735d6731 subpackages: - lib/go/thrift +- name: github.com/asaskevich/govalidator + version: 808e7b820405fbd763f8a3c95531df8f87e675f1 - name: github.com/beorn7/perks version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 subpackages: @@ -17,7 +19,7 @@ imports: - assert - require - name: github.com/davecgh/go-spew - version: a476722483882dd40b8111f0eb64e1d7f43f56e4 + version: ecdeabc65495df2dec95d7c4a4c3e021903035e5 subpackages: - spew - name: github.com/fsnotify/fsnotify @@ -30,6 +32,32 @@ imports: - metrics/generic - metrics/internal/lv - metrics/prometheus +- name: github.com/go-openapi/analysis + version: 8ed83f2ea9f00f945516462951a288eaa68bf0d6 +- name: github.com/go-openapi/errors + version: 03cfca65330da08a5a440053faf994a3c682b5bf +- name: github.com/go-openapi/jsonpointer + version: 779f45308c19820f1a69e9a4cd965f496e0da10f +- name: github.com/go-openapi/jsonreference + version: 36d33bfe519efae5632669801b180bf1a245da3b +- name: github.com/go-openapi/loads + version: c3e1ca4c0b6160cac10aeef7e8b425cc95b9c820 +- name: github.com/go-openapi/runtime + version: e8231e16de5bcda9839e03ae07c5c96a1fd82041 + subpackages: + - middleware + - middleware/denco + - middleware/header + - middleware/untyped + - security +- name: github.com/go-openapi/spec + version: a4fa9574c7aa73b2fc54e251eb9524d0482bb592 +- name: github.com/go-openapi/strfmt + version: 610b6cacdcde6852f4de68998bd20ce1dac85b22 +- name: github.com/go-openapi/swag + version: f3f9494671f93fcff853e3c6e9e948b3eb71e590 +- name: github.com/go-openapi/validate + version: a762d5dd38f7c7e9e1a13c3e98344cfbe4aa15d9 - name: github.com/gocql/gocql version: 4d2d1ac71932f7c4a6c7feb0d654462e4116c58b subpackages: @@ -67,6 +95,12 @@ imports: version: cfb55aafdaf3ec08f0db22699ab822c50091b1c4 - name: github.com/magiconair/properties version: 9c47895dc1ce54302908ab8a43385d1f5df2c11c +- name: github.com/mailru/easyjson + version: 5f62e4f3afa2f576dc86531b7df4d966b19ef8f8 + subpackages: + - buffer + - jlexer + - jwriter - name: github.com/matttproud/golang_protobuf_extensions version: c12348ce28de40eed0136aa2b644d0ee0650e56c subpackages: @@ -113,6 +147,10 @@ imports: version: a1dba9ce8baed984a2495b658c82687f8157b98f subpackages: - xfs +- name: github.com/PuerkitoBio/purell + version: fd18e053af8a4ff11039269006e8037ff374ce0e +- name: github.com/PuerkitoBio/urlesc + version: de5bf2ad457846296e2031421a34e2568e304e35 - name: github.com/spf13/afero version: 90dd71edc4d0a8b3511dc12ea15d617d03be09e0 subpackages: @@ -130,7 +168,7 @@ imports: - name: github.com/stretchr/objx version: 1a9d0bb9f541897e62256577b352fdbc1fb4fd94 - name: github.com/stretchr/testify - version: 890a5c3458b43e6104ff5da8dfa139d013d77544 + version: 2aa2c176b9dab406a6970f6a55f513e8a8c8b18f subpackages: - assert - mock @@ -188,10 +226,11 @@ imports: - zapcore - zaptest - name: golang.org/x/net - version: 8351a756f30f1297fe94bbf4b767ec589c6ea6d0 + version: a337091b0525af65de94df2eb7e98bd9962dcbe2 subpackages: - context - context/ctxhttp + - idna - name: golang.org/x/sys version: d4feaf1a7e61e1d9e79e6c4e76c6349e9cab0a03 subpackages: @@ -199,16 +238,24 @@ imports: - name: golang.org/x/text version: 44f4f658a783b0cee41fe0a23b8fc91d9c120558 subpackages: + - secure/bidirule - transform + - unicode/bidi - unicode/norm + - width - name: gopkg.in/inf.v0 version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 +- name: gopkg.in/mgo.v2 + version: 3f83fa5005286a7fe593b055f0d7771a7dce4655 + subpackages: + - bson + - internal/json - name: gopkg.in/olivere/elastic.v5 version: f1fd7305d0b6270192b27010fe1193568b18e4b6 subpackages: - uritemplates - name: gopkg.in/yaml.v2 - version: 3b4ad1db5b2a649883ff3782f5f9f6fb52be71af + version: eb3733d160e74a9c7e442f435eb3bea458e1d19f testImports: - name: github.com/kr/text version: 7cafcd837844e784b526369c9bce262804aebc60 diff --git a/glide.yaml b/glide.yaml index de2fc1b9dc5..8d3b51e842c 100644 --- a/glide.yaml +++ b/glide.yaml @@ -42,3 +42,10 @@ import: version: v5.0.39 - package: github.com/spf13/cobra - package: github.com/spf13/viper +- package: github.com/go-openapi/errors +- package: github.com/go-openapi/runtime +- package: github.com/go-openapi/spec +- package: github.com/go-openapi/strfmt +- package: github.com/go-openapi/swag +- package: github.com/go-openapi/validate +- package: github.com/go-openapi/loads diff --git a/plugin/storage/es/spanstore/reader.go b/plugin/storage/es/spanstore/reader.go index a42ac336c1b..dd1c10f1ea1 100644 --- a/plugin/storage/es/spanstore/reader.go +++ b/plugin/storage/es/spanstore/reader.go @@ -325,7 +325,9 @@ func (s *SpanReader) findTraceIDs(traceQuery *spanstore.TraceQueryParameters) ([ if err != nil { return nil, errors.Wrap(err, "Search service failed") } - + if searchResult.Aggregations == nil { + return []string{}, nil + } bucket, found := searchResult.Aggregations.Terms(traceIDAggregation) if !found { return nil, ErrUnableToFindTraceIDAggregation diff --git a/plugin/storage/es/spanstore/reader_test.go b/plugin/storage/es/spanstore/reader_test.go index 8144ec3be12..159f4d51d39 100644 --- a/plugin/storage/es/spanstore/reader_test.go +++ b/plugin/storage/es/spanstore/reader_test.go @@ -843,3 +843,28 @@ func TestSpanReader_buildTagQuery(t *testing.T) { assert.EqualValues(t, expected, actual) }) } + +func TestSpanReader_GetEmptyIndex(t *testing.T) { + withSpanReader(func(r *spanReaderTest) { + mockSearchService(r). + Return(&elastic.SearchResult{}, nil) + mockMultiSearchService(r). + Return(&elastic.MultiSearchResult{ + Responses: []*elastic.SearchResult{}, + }, nil) + + traceQuery := &spanstore.TraceQueryParameters{ + ServiceName: serviceName, + Tags: map[string]string{ + "hello": "world", + }, + StartTimeMin: time.Now().Add(-1 * time.Hour), + StartTimeMax: time.Now(), + NumTraces: 2, + } + + services, err := r.reader.FindTraces(traceQuery) + require.NoError(t, err) + assert.Empty(t, services) + }) +} diff --git a/plugin/storage/es/spanstore/service_operation.go b/plugin/storage/es/spanstore/service_operation.go index 452635bbb8e..8e28dec1ecb 100644 --- a/plugin/storage/es/spanstore/service_operation.go +++ b/plugin/storage/es/spanstore/service_operation.go @@ -102,7 +102,9 @@ func (s *ServiceOperationStorage) getServices(indices []string) ([]string, error if err != nil { return nil, errors.Wrap(err, "Search service failed") } - + if searchResult.Aggregations == nil { + return []string{}, nil + } bucket, found := searchResult.Aggregations.Terms(servicesAggregation) if !found { return nil, errors.New("Could not find aggregation of " + servicesAggregation) @@ -132,6 +134,9 @@ func (s *ServiceOperationStorage) getOperations(indices []string, service string if err != nil { return nil, errors.Wrap(err, "Search service failed") } + if searchResult.Aggregations == nil { + return []string{}, nil + } bucket, found := searchResult.Aggregations.Terms(operationsAggregation) if !found { return nil, errors.New("Could not find aggregation of " + operationsAggregation) diff --git a/plugin/storage/es/spanstore/service_operation_test.go b/plugin/storage/es/spanstore/service_operation_test.go index 0fcbe0cdffc..6b2d8249963 100644 --- a/plugin/storage/es/spanstore/service_operation_test.go +++ b/plugin/storage/es/spanstore/service_operation_test.go @@ -110,3 +110,31 @@ func TestSpanReader_GetServices(t *testing.T) { func TestSpanReader_GetOperations(t *testing.T) { testGet(operationsAggregation, t) } + +func TestSpanReader_GetServicesEmptyIndex(t *testing.T) { + withSpanReader(func(r *spanReaderTest) { + mockSearchService(r). + Return(&elastic.SearchResult{}, nil) + mockMultiSearchService(r). + Return(&elastic.MultiSearchResult{ + Responses: []*elastic.SearchResult{}, + }, nil) + services, err := r.reader.GetServices() + require.NoError(t, err) + assert.Empty(t, services) + }) +} + +func TestSpanReader_GetOperationsEmptyIndex(t *testing.T) { + withSpanReader(func(r *spanReaderTest) { + mockSearchService(r). + Return(&elastic.SearchResult{}, nil) + mockMultiSearchService(r). + Return(&elastic.MultiSearchResult{ + Responses: []*elastic.SearchResult{}, + }, nil) + services, err := r.reader.GetOperations("foo") + require.NoError(t, err) + assert.Empty(t, services) + }) +} diff --git a/scripts/updateLicenses.sh b/scripts/updateLicenses.sh index 59140ba28dc..badef63705c 100755 --- a/scripts/updateLicenses.sh +++ b/scripts/updateLicenses.sh @@ -2,4 +2,4 @@ set -e -python scripts/updateLicense.py $(git ls-files "*\.go" | grep -v -e thrift-gen) +python scripts/updateLicense.py $(git ls-files "*\.go" | grep -v -e thrift-gen -e swagger-gen) diff --git a/swagger-gen/models/annotation.go b/swagger-gen/models/annotation.go new file mode 100644 index 00000000000..9fba5cb679b --- /dev/null +++ b/swagger-gen/models/annotation.go @@ -0,0 +1,75 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + strfmt "github.com/go-openapi/strfmt" + + "github.com/go-openapi/errors" + "github.com/go-openapi/swag" +) + +// Annotation Annotation +// +// Associates an event that explains latency with a timestamp. +// Unlike log statements, annotations are often codes. Ex. "ws" for WireSend +// +// Zipkin v1 core annotations such as "cs" and "sr" have been replaced with +// Span.Kind, which interprets timestamp and duration. +// +// swagger:model Annotation + +type Annotation struct { + + // Epoch **microseconds** of this event. + // + // For example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC + // + // This value should be set directly by instrumentation, using the most precise + // value possible. For example, gettimeofday or multiplying epoch millis by 1000. + // + Timestamp int64 `json:"timestamp,omitempty"` + + // Usually a short tag indicating an event, like "error" + // + // While possible to add larger data, such as garbage collection details, low + // cardinality event names both keep the size of spans down and also are easy + // to search against. + // + Value string `json:"value,omitempty"` +} + +/* polymorph Annotation timestamp false */ + +/* polymorph Annotation value false */ + +// Validate validates this annotation +func (m *Annotation) Validate(formats strfmt.Registry) error { + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// MarshalBinary interface implementation +func (m *Annotation) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Annotation) UnmarshalBinary(b []byte) error { + var res Annotation + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/swagger-gen/models/endpoint.go b/swagger-gen/models/endpoint.go new file mode 100644 index 00000000000..368f7ba4ff2 --- /dev/null +++ b/swagger-gen/models/endpoint.go @@ -0,0 +1,82 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + strfmt "github.com/go-openapi/strfmt" + + "github.com/go-openapi/errors" + "github.com/go-openapi/swag" +) + +// Endpoint Endpoint +// +// The network context of a node in the service graph +// swagger:model Endpoint + +type Endpoint struct { + + // The text representation of the primary IPv4 address associated with this + // a connection. Ex. 192.168.99.100 Absent if unknown. + // + IPV4 strfmt.IPv4 `json:"ipv4,omitempty"` + + // The text representation of the primary IPv6 address associated with this + // a connection. Ex. 2001:db8::c001 Absent if unknown. + // + // Prefer using the ipv4 field for mapped addresses. + // + IPV6 strfmt.IPv6 `json:"ipv6,omitempty"` + + // Depending on context, this could be a listen port or the client-side of a + // socket. Absent if unknown + // + Port int64 `json:"port,omitempty"` + + // Lower-case label of this node in the service graph, such as "favstar". Leave + // absent if unknown. + // + // This is a primary label for trace lookup and aggregation, so it should be + // intuitive and consistent. Many use a name from service discovery. + // + ServiceName string `json:"serviceName,omitempty"` +} + +/* polymorph Endpoint ipv4 false */ + +/* polymorph Endpoint ipv6 false */ + +/* polymorph Endpoint port false */ + +/* polymorph Endpoint serviceName false */ + +// Validate validates this endpoint +func (m *Endpoint) Validate(formats strfmt.Registry) error { + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// MarshalBinary interface implementation +func (m *Endpoint) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Endpoint) UnmarshalBinary(b []byte) error { + var res Endpoint + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/swagger-gen/models/list_of_spans.go b/swagger-gen/models/list_of_spans.go new file mode 100644 index 00000000000..984a25d0851 --- /dev/null +++ b/swagger-gen/models/list_of_spans.go @@ -0,0 +1,50 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "strconv" + + strfmt "github.com/go-openapi/strfmt" + + "github.com/go-openapi/errors" + "github.com/go-openapi/swag" +) + +// ListOfSpans ListOfSpans +// +// A list of spans with possibly different trace ids, in no particular order +// swagger:model ListOfSpans + +type ListOfSpans []*Span + +// Validate validates this list of spans +func (m ListOfSpans) Validate(formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + + if swag.IsZero(m[i]) { // not required + continue + } + + if m[i] != nil { + + if err := m[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } + return err + } + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/swagger-gen/models/span.go b/swagger-gen/models/span.go new file mode 100644 index 00000000000..e88e367573f --- /dev/null +++ b/swagger-gen/models/span.go @@ -0,0 +1,417 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "strconv" + + strfmt "github.com/go-openapi/strfmt" + + "github.com/go-openapi/errors" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// Span Span +// swagger:model Span + +type Span struct { + + // Associates events that explain latency with the time they happened. + // Unique: true + Annotations []*Annotation `json:"annotations"` + + // True is a request to store this span even if it overrides sampling policy. + // + // This is true when the `X-B3-Flags` header has a value of 1. + // + Debug bool `json:"debug,omitempty"` + + // Duration in **microseconds** of the critical path, if known. Durations of less + // than one are rounded up. Duration of children can be longer than their parents + // due to asynchronous operations. + // + // For example 150 milliseconds is 150000 microseconds. + // + // Minimum: 1 + Duration int64 `json:"duration,omitempty"` + + // Unique 64bit identifier for this operation within the trace. + // + // Encoded as 16 lowercase hex characters. For example ffdc9bb9a6453df3 + // + // Required: true + // Max Length: 16 + // Min Length: 16 + // Pattern: [a-z0-9]{16} + ID *string `json:"id"` + + // When present, clarifies timestamp, duration and remoteEndpoint. When + // absent, the span is local or incomplete. Unlike client and server, + // there is no direct critical path latency relationship between producer + // and consumer spans. + // + // * `CLIENT` + // * timestamp - The moment a request was sent (formerly "cs") + // * duration - When present indicates when a response was received (formerly "cr") + // * remoteEndpoint - Represents the server. Leave serviceName absent if unknown. + // * `SERVER` + // * timestamp - The moment a request was received (formerly "sr") + // * duration - When present indicates when a response was sent (formerly "ss") + // * remoteEndpoint - Represents the client. Leave serviceName absent if unknown. + // * `PRODUCER` + // * timestamp - The moment a message was sent to a destination (formerly "ms") + // * duration - When present represents delay sending the message, such as batching. + // * remoteEndpoint - Represents the broker. Leave serviceName absent if unknown. + // * `CONSUMER` + // * timestamp - The moment a message was received from an origin (formerly "mr") + // * duration - When present represents delay consuming the message, such as from backlog. + // * remoteEndpoint - Represents the broker. Leave serviceName absent if unknown. + // + Kind string `json:"kind,omitempty"` + + // The host that recorded this span, primarily for query by service name. + // + // Instrumentation should always record this. Usually, absent implies late data. + // The IP address corresponding to this is usually the site local or advertised + // service address. When present, the port indicates the listen port. + // + LocalEndpoint *Endpoint `json:"localEndpoint,omitempty"` + + // The logical operation this span represents in lowercase (e.g. rpc method). + // Leave absent if unknown. + // + // As these are lookup labels, take care to ensure names are low cardinality. + // For example, do not embed variables into the name. + // + Name string `json:"name,omitempty"` + + // The parent span ID or absent if this the root span in a trace. + // Max Length: 16 + // Min Length: 16 + // Pattern: [a-z0-9]{16} + ParentID string `json:"parentId,omitempty"` + + // When an RPC (or messaging) span, indicates the other side of the connection. + // + RemoteEndpoint *Endpoint `json:"remoteEndpoint,omitempty"` + + // True if we are contributing to a span started by another tracer (ex on a different host). + Shared bool `json:"shared,omitempty"` + + // Tags give your span context for search, viewing and analysis. + Tags Tags `json:"tags,omitempty"` + + // Epoch **microseconds** of the start of this span, possibly absent if incomplete. + // + // For example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC + // + // This value should be set directly by instrumentation, using the most precise + // value possible. For example, gettimeofday or multiplying epoch millis by 1000. + // + // There are three known edge-cases where this could be reported absent. + // * A span was allocated but never started (ex not yet received a timestamp) + // * The span's start event was lost + // * Data about a completed span (ex tags) were sent after the fact + // + Timestamp int64 `json:"timestamp,omitempty"` + + // Randomly generated, unique identifier for a trace, set on all spans within it. + // + // Encoded as 16 or 32 lowercase hex characters corresponding to 64 or 128 bits. + // For example, a 128bit trace ID looks like 4e441824ec2b6a44ffdc9bb9a6453df3 + // + // Required: true + // Max Length: 32 + // Min Length: 16 + // Pattern: [a-z0-9]{16,32} + TraceID *string `json:"traceId"` +} + +/* polymorph Span annotations false */ + +/* polymorph Span debug false */ + +/* polymorph Span duration false */ + +/* polymorph Span id false */ + +/* polymorph Span kind false */ + +/* polymorph Span localEndpoint false */ + +/* polymorph Span name false */ + +/* polymorph Span parentId false */ + +/* polymorph Span remoteEndpoint false */ + +/* polymorph Span shared false */ + +/* polymorph Span tags false */ + +/* polymorph Span timestamp false */ + +/* polymorph Span traceId false */ + +// Validate validates this span +func (m *Span) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAnnotations(formats); err != nil { + // prop + res = append(res, err) + } + + if err := m.validateDuration(formats); err != nil { + // prop + res = append(res, err) + } + + if err := m.validateID(formats); err != nil { + // prop + res = append(res, err) + } + + if err := m.validateKind(formats); err != nil { + // prop + res = append(res, err) + } + + if err := m.validateLocalEndpoint(formats); err != nil { + // prop + res = append(res, err) + } + + if err := m.validateParentID(formats); err != nil { + // prop + res = append(res, err) + } + + if err := m.validateRemoteEndpoint(formats); err != nil { + // prop + res = append(res, err) + } + + if err := m.validateTraceID(formats); err != nil { + // prop + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Span) validateAnnotations(formats strfmt.Registry) error { + + if swag.IsZero(m.Annotations) { // not required + return nil + } + + if err := validate.UniqueItems("annotations", "body", m.Annotations); err != nil { + return err + } + + for i := 0; i < len(m.Annotations); i++ { + + if swag.IsZero(m.Annotations[i]) { // not required + continue + } + + if m.Annotations[i] != nil { + + if err := m.Annotations[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("annotations" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +func (m *Span) validateDuration(formats strfmt.Registry) error { + + if swag.IsZero(m.Duration) { // not required + return nil + } + + if err := validate.MinimumInt("duration", "body", int64(m.Duration), 1, false); err != nil { + return err + } + + return nil +} + +func (m *Span) validateID(formats strfmt.Registry) error { + + if err := validate.Required("id", "body", m.ID); err != nil { + return err + } + + if err := validate.MinLength("id", "body", string(*m.ID), 16); err != nil { + return err + } + + if err := validate.MaxLength("id", "body", string(*m.ID), 16); err != nil { + return err + } + + if err := validate.Pattern("id", "body", string(*m.ID), `[a-z0-9]{16}`); err != nil { + return err + } + + return nil +} + +var spanTypeKindPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["CLIENT","SERVER","PRODUCER","CONSUMER"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + spanTypeKindPropEnum = append(spanTypeKindPropEnum, v) + } +} + +const ( + // SpanKindCLIENT captures enum value "CLIENT" + SpanKindCLIENT string = "CLIENT" + // SpanKindSERVER captures enum value "SERVER" + SpanKindSERVER string = "SERVER" + // SpanKindPRODUCER captures enum value "PRODUCER" + SpanKindPRODUCER string = "PRODUCER" + // SpanKindCONSUMER captures enum value "CONSUMER" + SpanKindCONSUMER string = "CONSUMER" +) + +// prop value enum +func (m *Span) validateKindEnum(path, location string, value string) error { + if err := validate.Enum(path, location, value, spanTypeKindPropEnum); err != nil { + return err + } + return nil +} + +func (m *Span) validateKind(formats strfmt.Registry) error { + + if swag.IsZero(m.Kind) { // not required + return nil + } + + // value enum + if err := m.validateKindEnum("kind", "body", m.Kind); err != nil { + return err + } + + return nil +} + +func (m *Span) validateLocalEndpoint(formats strfmt.Registry) error { + + if swag.IsZero(m.LocalEndpoint) { // not required + return nil + } + + if m.LocalEndpoint != nil { + + if err := m.LocalEndpoint.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("localEndpoint") + } + return err + } + } + + return nil +} + +func (m *Span) validateParentID(formats strfmt.Registry) error { + + if swag.IsZero(m.ParentID) { // not required + return nil + } + + if err := validate.MinLength("parentId", "body", string(m.ParentID), 16); err != nil { + return err + } + + if err := validate.MaxLength("parentId", "body", string(m.ParentID), 16); err != nil { + return err + } + + if err := validate.Pattern("parentId", "body", string(m.ParentID), `[a-z0-9]{16}`); err != nil { + return err + } + + return nil +} + +func (m *Span) validateRemoteEndpoint(formats strfmt.Registry) error { + + if swag.IsZero(m.RemoteEndpoint) { // not required + return nil + } + + if m.RemoteEndpoint != nil { + + if err := m.RemoteEndpoint.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("remoteEndpoint") + } + return err + } + } + + return nil +} + +func (m *Span) validateTraceID(formats strfmt.Registry) error { + + if err := validate.Required("traceId", "body", m.TraceID); err != nil { + return err + } + + if err := validate.MinLength("traceId", "body", string(*m.TraceID), 16); err != nil { + return err + } + + if err := validate.MaxLength("traceId", "body", string(*m.TraceID), 32); err != nil { + return err + } + + if err := validate.Pattern("traceId", "body", string(*m.TraceID), `[a-z0-9]{16,32}`); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *Span) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Span) UnmarshalBinary(b []byte) error { + var res Span + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/swagger-gen/models/tags.go b/swagger-gen/models/tags.go new file mode 100644 index 00000000000..2207166c335 --- /dev/null +++ b/swagger-gen/models/tags.go @@ -0,0 +1,27 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + strfmt "github.com/go-openapi/strfmt" +) + +// Tags Tags +// +// Adds context to a span, for search, viewing and analysis. +// +// For example, a key "your_app.version" would let you lookup traces by version. +// A tag "sql.query" isn't searchable, but it can help in debugging when viewing +// a trace. +// +// swagger:model Tags + +type Tags map[string]string + +// Validate validates this tags +func (m Tags) Validate(formats strfmt.Registry) error { + return nil +} diff --git a/swagger-gen/restapi/doc.go b/swagger-gen/restapi/doc.go new file mode 100644 index 00000000000..ef19e6e999c --- /dev/null +++ b/swagger-gen/restapi/doc.go @@ -0,0 +1,24 @@ +// Code generated by go-swagger; DO NOT EDIT. + +/* +Package restapi Zipkin API +Zipkin's v2 api currently includes a POST endpoint that can receive spans. + + + + Schemes: + http + https + Host: localhost:9411 + BasePath: /api/v2 + Version: 1.0.0 + + Consumes: + - application/json + + Produces: + - application/json + +swagger:meta +*/ +package restapi diff --git a/swagger-gen/restapi/embedded_spec.go b/swagger-gen/restapi/embedded_spec.go new file mode 100644 index 00000000000..225e121d617 --- /dev/null +++ b/swagger-gen/restapi/embedded_spec.go @@ -0,0 +1,406 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package restapi + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" +) + +// SwaggerJSON embedded version of the swagger document used at generation time +var SwaggerJSON json.RawMessage + +func init() { + SwaggerJSON = json.RawMessage([]byte(`{ + "consumes": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "Zipkin's v2 api currently includes a POST endpoint that can receive spans.\n", + "title": "Zipkin API", + "version": "1.0.0" + }, + "host": "localhost:9411", + "basePath": "/api/v2", + "paths": { + "/dependencies": { + "get": { + "description": "Returns service links derived from spans.\n", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "only return links from spans where ` + "`" + `Span.timestamp` + "`" + ` are at or before\nthis time in epoch milliseconds.\n", + "name": "endTs", + "in": "query", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "only return links where all Span.timestamp are at or after\n(` + "`" + `endTs - * lookback` + "`" + `) in milliseconds. Defaults to ` + "`" + `endTs` + "`" + `, limited\nto a system parameter ` + "`" + `QUERY_LOOKBACK` + "`" + `\n", + "name": "lookback", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "title": "ListOfDependencyLinks", + "items": { + "$ref": "#/definitions/DependencyLink" + } + } + } + } + } + }, + "/services": { + "get": { + "description": "Returns a list of all service names associated with span endpoints.\n", + "responses": { + "200": { + "description": "Succes", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request Error" + } + } + } + }, + "/spans": { + "get": { + "description": "Get all the span names recorded by a particular service", + "parameters": [ + { + "type": "string", + "description": "Ex favstar (required) - Lower-case label of a node in the service\ngraph. The /services endpoint enumerates possible input values.\n", + "name": "serviceName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request Error" + } + } + }, + "post": { + "description": "Uploads a list of spans encoded per content-type, for example json.\n", + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "A list of spans that belong to any trace.", + "name": "spans", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ListOfSpans" + } + } + ], + "responses": { + "202": { + "description": "Accepted" + } + } + } + }, + "/trace/{traceId}": { + "get": { + "parameters": [ + { + "maxLength": 32, + "minLength": 16, + "pattern": "[a-z0-9]{16,32}", + "type": "string", + "description": "Trace identifier, set on all spans within it.\n\nEncoded as 16 or 32 lowercase hex characters corresponding to 64 or 128 bits.\nFor example, a 128bit trace ID looks like 4e441824ec2b6a44ffdc9bb9a6453df3\n", + "name": "traceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Trace" + } + }, + "404": { + "description": "` + "`" + `traceId` + "`" + ` not found" + } + } + } + }, + "/traces": { + "get": { + "description": "Invoking this request retrieves traces matching the below filters.\n\nResults should be filtered against endTs, subject to limit and\nlookback. For example, if endTs is 10:20 today, limit is 10, and\nlookback is 7 days, traces returned should be those nearest to 10:20\ntoday, not 10:20 a week ago.\n\nTime units of endTs and lookback are milliseconds as opposed to\nmicroseconds, the grain of Span.timestamp. Milliseconds is a more\nfamiliar and supported granularity for query, index and windowing\nfunctions\n", + "parameters": [ + { + "type": "string", + "description": "Ex favstar (required) - Lower-case label of a node in the service\ngraph. The /services endpoint enumerates possible input values.\n", + "name": "serviceName", + "in": "query" + }, + { + "type": "string", + "description": "Ex get - name of a span in a trace.\nOnly return traces that contains spans with this name.\n", + "name": "spanName", + "in": "query" + }, + { + "type": "string", + "description": "Ex. ` + "`" + `http.uri=/foo and retried` + "`" + ` - If key/value (has an ` + "`" + `=` + "`" + `),\nconstrains against Span.tags entres. If just a word, constrains\nagainst Span.annotations[].value or Span.tags[].key. Any values are\nAND against eachother. This means a span in the trace must match\nall of these.\n", + "name": "annotationQuery", + "in": "query" + }, + { + "type": "integer", + "description": "Ex. 100000 (for 100ms). Only return traces whose ` + "`" + `Span.duration` + "`" + ` is\ngreater than or equal to minDuration microseconds.\n", + "name": "minDuration", + "in": "query" + }, + { + "type": "integer", + "description": "Only return traces whose Span.duration is less than or equal to\n` + "`" + `maxDuration` + "`" + ` microseconds. Only valid with minDuration.\n", + "name": "maxDuration", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "Only return traces where all Span.timestamp are at or before this\ntime in epoch milliseconds. Defaults to current time.\n", + "name": "endTs", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "Only return traces where all Span.timestamp are at or after (endTs\n- * lookback) in milliseconds. Defaults to endTs, limited to a\nsystem parameter QUERY_LOOKBACK\n", + "name": "lookback", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Maximum number of traces to return. Defaults to 10\n", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ListOfTraces" + } + } + } + } + } + }, + "definitions": { + "Annotation": { + "description": "Associates an event that explains latency with a timestamp.\nUnlike log statements, annotations are often codes. Ex. \"ws\" for WireSend\n\nZipkin v1 core annotations such as \"cs\" and \"sr\" have been replaced with\nSpan.Kind, which interprets timestamp and duration.\n", + "type": "object", + "title": "Annotation", + "properties": { + "timestamp": { + "description": "Epoch **microseconds** of this event.\n\nFor example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC\n\nThis value should be set directly by instrumentation, using the most precise\nvalue possible. For example, gettimeofday or multiplying epoch millis by 1000.\n", + "type": "integer" + }, + "value": { + "description": "Usually a short tag indicating an event, like \"error\"\n\nWhile possible to add larger data, such as garbage collection details, low\ncardinality event names both keep the size of spans down and also are easy\nto search against.\n", + "type": "string" + } + } + }, + "DependencyLink": { + "type": "object", + "title": "DependencyLink", + "properties": { + "callCount": { + "type": "integer" + }, + "child": { + "type": "string" + }, + "errorCount": { + "type": "integer" + }, + "parent": { + "type": "string" + } + } + }, + "Endpoint": { + "description": "The network context of a node in the service graph", + "type": "object", + "title": "Endpoint", + "properties": { + "ipv4": { + "description": "The text representation of the primary IPv4 address associated with this\na connection. Ex. 192.168.99.100 Absent if unknown.\n", + "type": "string", + "format": "ipv4" + }, + "ipv6": { + "description": "The text representation of the primary IPv6 address associated with this\na connection. Ex. 2001:db8::c001 Absent if unknown.\n\nPrefer using the ipv4 field for mapped addresses.\n", + "type": "string", + "format": "ipv6" + }, + "port": { + "description": "Depending on context, this could be a listen port or the client-side of a\nsocket. Absent if unknown\n", + "type": "integer" + }, + "serviceName": { + "description": "Lower-case label of this node in the service graph, such as \"favstar\". Leave\nabsent if unknown.\n\nThis is a primary label for trace lookup and aggregation, so it should be\nintuitive and consistent. Many use a name from service discovery.\n", + "type": "string" + } + } + }, + "ListOfSpans": { + "description": "A list of spans with possibly different trace ids, in no particular order", + "type": "array", + "title": "ListOfSpans", + "items": { + "$ref": "#/definitions/Span" + } + }, + "ListOfTraces": { + "type": "array", + "title": "ListOfTraces", + "items": { + "$ref": "#/definitions/Trace" + } + }, + "Span": { + "type": "object", + "title": "Span", + "required": [ + "traceId", + "id" + ], + "properties": { + "annotations": { + "description": "Associates events that explain latency with the time they happened.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/Annotation" + } + }, + "debug": { + "description": "True is a request to store this span even if it overrides sampling policy.\n\nThis is true when the ` + "`" + `X-B3-Flags` + "`" + ` header has a value of 1.\n", + "type": "boolean" + }, + "duration": { + "description": "Duration in **microseconds** of the critical path, if known. Durations of less\nthan one are rounded up. Duration of children can be longer than their parents\ndue to asynchronous operations.\n\nFor example 150 milliseconds is 150000 microseconds.\n", + "type": "integer", + "format": "int64", + "minimum": 1 + }, + "id": { + "description": "Unique 64bit identifier for this operation within the trace.\n\nEncoded as 16 lowercase hex characters. For example ffdc9bb9a6453df3\n", + "type": "string", + "maxLength": 16, + "minLength": 16, + "pattern": "[a-z0-9]{16}" + }, + "kind": { + "description": "When present, clarifies timestamp, duration and remoteEndpoint. When\nabsent, the span is local or incomplete. Unlike client and server,\nthere is no direct critical path latency relationship between producer\nand consumer spans.\n\n* ` + "`" + `CLIENT` + "`" + `\n * timestamp - The moment a request was sent (formerly \"cs\")\n * duration - When present indicates when a response was received (formerly \"cr\")\n * remoteEndpoint - Represents the server. Leave serviceName absent if unknown.\n* ` + "`" + `SERVER` + "`" + `\n * timestamp - The moment a request was received (formerly \"sr\")\n * duration - When present indicates when a response was sent (formerly \"ss\")\n * remoteEndpoint - Represents the client. Leave serviceName absent if unknown.\n* ` + "`" + `PRODUCER` + "`" + `\n * timestamp - The moment a message was sent to a destination (formerly \"ms\")\n * duration - When present represents delay sending the message, such as batching.\n * remoteEndpoint - Represents the broker. Leave serviceName absent if unknown.\n* ` + "`" + `CONSUMER` + "`" + `\n * timestamp - The moment a message was received from an origin (formerly \"mr\")\n * duration - When present represents delay consuming the message, such as from backlog.\n * remoteEndpoint - Represents the broker. Leave serviceName absent if unknown.\n", + "type": "string", + "enum": [ + "CLIENT", + "SERVER", + "PRODUCER", + "CONSUMER" + ] + }, + "localEndpoint": { + "description": "The host that recorded this span, primarily for query by service name.\n\nInstrumentation should always record this. Usually, absent implies late data.\nThe IP address corresponding to this is usually the site local or advertised\nservice address. When present, the port indicates the listen port.\n", + "$ref": "#/definitions/Endpoint" + }, + "name": { + "description": "The logical operation this span represents in lowercase (e.g. rpc method).\nLeave absent if unknown.\n\nAs these are lookup labels, take care to ensure names are low cardinality.\nFor example, do not embed variables into the name.\n", + "type": "string" + }, + "parentId": { + "description": "The parent span ID or absent if this the root span in a trace.", + "type": "string", + "maxLength": 16, + "minLength": 16, + "pattern": "[a-z0-9]{16}" + }, + "remoteEndpoint": { + "description": "When an RPC (or messaging) span, indicates the other side of the connection.\n", + "$ref": "#/definitions/Endpoint" + }, + "shared": { + "description": "True if we are contributing to a span started by another tracer (ex on a different host).", + "type": "boolean" + }, + "tags": { + "description": "Tags give your span context for search, viewing and analysis.", + "$ref": "#/definitions/Tags" + }, + "timestamp": { + "description": "Epoch **microseconds** of the start of this span, possibly absent if incomplete.\n\nFor example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC\n\nThis value should be set directly by instrumentation, using the most precise\nvalue possible. For example, gettimeofday or multiplying epoch millis by 1000.\n\nThere are three known edge-cases where this could be reported absent.\n * A span was allocated but never started (ex not yet received a timestamp)\n * The span's start event was lost\n * Data about a completed span (ex tags) were sent after the fact\n", + "type": "integer", + "format": "int64" + }, + "traceId": { + "description": "Randomly generated, unique identifier for a trace, set on all spans within it.\n\nEncoded as 16 or 32 lowercase hex characters corresponding to 64 or 128 bits.\nFor example, a 128bit trace ID looks like 4e441824ec2b6a44ffdc9bb9a6453df3\n", + "type": "string", + "maxLength": 32, + "minLength": 16, + "pattern": "[a-z0-9]{16,32}" + } + } + }, + "Tags": { + "description": "Adds context to a span, for search, viewing and analysis.\n\nFor example, a key \"your_app.version\" would let you lookup traces by version.\nA tag \"sql.query\" isn't searchable, but it can help in debugging when viewing\na trace.\n", + "type": "object", + "title": "Tags", + "additionalProperties": { + "type": "string" + } + }, + "Trace": { + "description": "List of spans who have the same trace ID.", + "type": "array", + "title": "Trace", + "items": { + "$ref": "#/definitions/Span" + } + } + } +}`)) +} diff --git a/swagger-gen/restapi/operations/post_spans.go b/swagger-gen/restapi/operations/post_spans.go new file mode 100644 index 00000000000..d06fae08a8e --- /dev/null +++ b/swagger-gen/restapi/operations/post_spans.go @@ -0,0 +1,59 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + middleware "github.com/go-openapi/runtime/middleware" +) + +// PostSpansHandlerFunc turns a function with the right signature into a post spans handler +type PostSpansHandlerFunc func(PostSpansParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn PostSpansHandlerFunc) Handle(params PostSpansParams) middleware.Responder { + return fn(params) +} + +// PostSpansHandler interface for that can handle valid post spans params +type PostSpansHandler interface { + Handle(PostSpansParams) middleware.Responder +} + +// NewPostSpans creates a new http.Handler for the post spans operation +func NewPostSpans(ctx *middleware.Context, handler PostSpansHandler) *PostSpans { + return &PostSpans{Context: ctx, Handler: handler} +} + +/*PostSpans swagger:route POST /spans postSpans + +Uploads a list of spans encoded per content-type, for example json. + + +*/ +type PostSpans struct { + Context *middleware.Context + Handler PostSpansHandler +} + +func (o *PostSpans) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewPostSpansParams() + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/swagger-gen/restapi/operations/post_spans_parameters.go b/swagger-gen/restapi/operations/post_spans_parameters.go new file mode 100644 index 00000000000..cfae7c69553 --- /dev/null +++ b/swagger-gen/restapi/operations/post_spans_parameters.go @@ -0,0 +1,73 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + + "github.com/jaegertracing/jaeger/swagger-gen/models" +) + +// NewPostSpansParams creates a new PostSpansParams object +// with the default values initialized. +func NewPostSpansParams() PostSpansParams { + var () + return PostSpansParams{} +} + +// PostSpansParams contains all the bound params for the post spans operation +// typically these are obtained from a http.Request +// +// swagger:parameters PostSpans +type PostSpansParams struct { + + // HTTP Request Object + HTTPRequest *http.Request + + /*A list of spans that belong to any trace. + Required: true + In: body + */ + Spans models.ListOfSpans +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls +func (o *PostSpansParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.ListOfSpans + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("spans", "body")) + } else { + res = append(res, errors.NewParseError("spans", "body", "", err)) + } + + } else { + + if len(res) == 0 { + o.Spans = body + } + } + + } else { + res = append(res, errors.Required("spans", "body")) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/swagger-gen/restapi/operations/post_spans_responses.go b/swagger-gen/restapi/operations/post_spans_responses.go new file mode 100644 index 00000000000..924e7171a4f --- /dev/null +++ b/swagger-gen/restapi/operations/post_spans_responses.go @@ -0,0 +1,33 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" +) + +// PostSpansAcceptedCode is the HTTP code returned for type PostSpansAccepted +const PostSpansAcceptedCode int = 202 + +/*PostSpansAccepted Accepted + +swagger:response postSpansAccepted +*/ +type PostSpansAccepted struct { +} + +// NewPostSpansAccepted creates PostSpansAccepted with default headers values +func NewPostSpansAccepted() *PostSpansAccepted { + return &PostSpansAccepted{} +} + +// WriteResponse to the client +func (o *PostSpansAccepted) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(202) +} diff --git a/swagger-gen/restapi/operations/zipkin_api.go b/swagger-gen/restapi/operations/zipkin_api.go new file mode 100644 index 00000000000..0ae3040869c --- /dev/null +++ b/swagger-gen/restapi/operations/zipkin_api.go @@ -0,0 +1,253 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "net/http" + "strings" + + errors "github.com/go-openapi/errors" + loads "github.com/go-openapi/loads" + runtime "github.com/go-openapi/runtime" + middleware "github.com/go-openapi/runtime/middleware" + security "github.com/go-openapi/runtime/security" + spec "github.com/go-openapi/spec" + strfmt "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewZipkinAPI creates a new Zipkin instance +func NewZipkinAPI(spec *loads.Document) *ZipkinAPI { + return &ZipkinAPI{ + handlers: make(map[string]map[string]http.Handler), + formats: strfmt.Default, + defaultConsumes: "application/json", + defaultProduces: "application/json", + ServerShutdown: func() {}, + spec: spec, + ServeError: errors.ServeError, + BasicAuthenticator: security.BasicAuth, + APIKeyAuthenticator: security.APIKeyAuth, + BearerAuthenticator: security.BearerAuth, + JSONConsumer: runtime.JSONConsumer(), + JSONProducer: runtime.JSONProducer(), + PostSpansHandler: PostSpansHandlerFunc(func(params PostSpansParams) middleware.Responder { + return middleware.NotImplemented("operation PostSpans has not yet been implemented") + }), + } +} + +/*ZipkinAPI Zipkin's v2 api currently includes a POST endpoint that can receive spans. + */ +type ZipkinAPI struct { + spec *loads.Document + context *middleware.Context + handlers map[string]map[string]http.Handler + formats strfmt.Registry + defaultConsumes string + defaultProduces string + Middleware func(middleware.Builder) http.Handler + + // BasicAuthenticator generates a runtime.Authenticator from the supplied basic auth function. + // It has a default implemention in the security package, however you can replace it for your particular usage. + BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator + // APIKeyAuthenticator generates a runtime.Authenticator from the supplied token auth function. + // It has a default implemention in the security package, however you can replace it for your particular usage. + APIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator + // BearerAuthenticator generates a runtime.Authenticator from the supplied bearer token auth function. + // It has a default implemention in the security package, however you can replace it for your particular usage. + BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator + + // JSONConsumer registers a consumer for a "application/json" mime type + JSONConsumer runtime.Consumer + + // JSONProducer registers a producer for a "application/json" mime type + JSONProducer runtime.Producer + + // PostSpansHandler sets the operation handler for the post spans operation + PostSpansHandler PostSpansHandler + + // ServeError is called when an error is received, there is a default handler + // but you can set your own with this + ServeError func(http.ResponseWriter, *http.Request, error) + + // ServerShutdown is called when the HTTP(S) server is shut down and done + // handling all active connections and does not accept connections any more + ServerShutdown func() + + // Custom command line argument groups with their descriptions + CommandLineOptionsGroups []swag.CommandLineOptionsGroup + + // User defined logger function. + Logger func(string, ...interface{}) +} + +// SetDefaultProduces sets the default produces media type +func (o *ZipkinAPI) SetDefaultProduces(mediaType string) { + o.defaultProduces = mediaType +} + +// SetDefaultConsumes returns the default consumes media type +func (o *ZipkinAPI) SetDefaultConsumes(mediaType string) { + o.defaultConsumes = mediaType +} + +// SetSpec sets a spec that will be served for the clients. +func (o *ZipkinAPI) SetSpec(spec *loads.Document) { + o.spec = spec +} + +// DefaultProduces returns the default produces media type +func (o *ZipkinAPI) DefaultProduces() string { + return o.defaultProduces +} + +// DefaultConsumes returns the default consumes media type +func (o *ZipkinAPI) DefaultConsumes() string { + return o.defaultConsumes +} + +// Formats returns the registered string formats +func (o *ZipkinAPI) Formats() strfmt.Registry { + return o.formats +} + +// RegisterFormat registers a custom format validator +func (o *ZipkinAPI) RegisterFormat(name string, format strfmt.Format, validator strfmt.Validator) { + o.formats.Add(name, format, validator) +} + +// Validate validates the registrations in the ZipkinAPI +func (o *ZipkinAPI) Validate() error { + var unregistered []string + + if o.JSONConsumer == nil { + unregistered = append(unregistered, "JSONConsumer") + } + + if o.JSONProducer == nil { + unregistered = append(unregistered, "JSONProducer") + } + + if o.PostSpansHandler == nil { + unregistered = append(unregistered, "PostSpansHandler") + } + + if len(unregistered) > 0 { + return fmt.Errorf("missing registration: %s", strings.Join(unregistered, ", ")) + } + + return nil +} + +// ServeErrorFor gets a error handler for a given operation id +func (o *ZipkinAPI) ServeErrorFor(operationID string) func(http.ResponseWriter, *http.Request, error) { + return o.ServeError +} + +// AuthenticatorsFor gets the authenticators for the specified security schemes +func (o *ZipkinAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator { + + return nil + +} + +// Authorizer returns the registered authorizer +func (o *ZipkinAPI) Authorizer() runtime.Authorizer { + + return nil + +} + +// ConsumersFor gets the consumers for the specified media types +func (o *ZipkinAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer { + + result := make(map[string]runtime.Consumer) + for _, mt := range mediaTypes { + switch mt { + + case "application/json": + result["application/json"] = o.JSONConsumer + + } + } + return result + +} + +// ProducersFor gets the producers for the specified media types +func (o *ZipkinAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer { + + result := make(map[string]runtime.Producer) + for _, mt := range mediaTypes { + switch mt { + + case "application/json": + result["application/json"] = o.JSONProducer + + } + } + return result + +} + +// HandlerFor gets a http.Handler for the provided operation method and path +func (o *ZipkinAPI) HandlerFor(method, path string) (http.Handler, bool) { + if o.handlers == nil { + return nil, false + } + um := strings.ToUpper(method) + if _, ok := o.handlers[um]; !ok { + return nil, false + } + if path == "/" { + path = "" + } + h, ok := o.handlers[um][path] + return h, ok +} + +// Context returns the middleware context for the zipkin API +func (o *ZipkinAPI) Context() *middleware.Context { + if o.context == nil { + o.context = middleware.NewRoutableContext(o.spec, o, nil) + } + + return o.context +} + +func (o *ZipkinAPI) initHandlerCache() { + o.Context() // don't care about the result, just that the initialization happened + + if o.handlers == nil { + o.handlers = make(map[string]map[string]http.Handler) + } + + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/spans"] = NewPostSpans(o.context, o.PostSpansHandler) + +} + +// Serve creates a http handler to serve the API over HTTP +// can be used directly in http.ListenAndServe(":8000", api.Serve(nil)) +func (o *ZipkinAPI) Serve(builder middleware.Builder) http.Handler { + o.Init() + + if o.Middleware != nil { + return o.Middleware(builder) + } + return o.context.APIHandler(builder) +} + +// Init allows you to just initialize the handler cache, you can then recompose the middelware as you see fit +func (o *ZipkinAPI) Init() { + if len(o.handlers) == 0 { + o.initHandlerCache() + } +}