284 lines
7.9 KiB
Go
284 lines
7.9 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package zipkinreceiver
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"compress/zlib"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
jaegerzipkin "github.com/jaegertracing/jaeger/model/converter/thrift/zipkin"
|
|
zipkinmodel "github.com/openzipkin/zipkin-go/model"
|
|
"github.com/openzipkin/zipkin-go/proto/zipkin_proto3"
|
|
|
|
"go.opentelemetry.io/collector/client"
|
|
"go.opentelemetry.io/collector/component"
|
|
"go.opentelemetry.io/collector/component/componenterror"
|
|
"go.opentelemetry.io/collector/consumer"
|
|
"go.opentelemetry.io/collector/consumer/pdata"
|
|
"go.opentelemetry.io/collector/obsreport"
|
|
"go.opentelemetry.io/collector/translator/trace/zipkin"
|
|
)
|
|
|
|
const (
|
|
receiverTransportV1Thrift = "http_v1_thrift"
|
|
receiverTransportV1JSON = "http_v1_json"
|
|
receiverTransportV2JSON = "http_v2_json"
|
|
receiverTransportV2PROTO = "http_v2_proto"
|
|
)
|
|
|
|
var errNextConsumerRespBody = []byte(`"Internal Server Error"`)
|
|
|
|
// ZipkinReceiver type is used to handle spans received in the Zipkin format.
|
|
type ZipkinReceiver struct {
|
|
// mu protects the fields of this struct
|
|
mu sync.Mutex
|
|
|
|
// addr is the address onto which the HTTP server will be bound
|
|
host component.Host
|
|
nextConsumer consumer.Traces
|
|
instanceName string
|
|
|
|
startOnce sync.Once
|
|
stopOnce sync.Once
|
|
shutdownWG sync.WaitGroup
|
|
server *http.Server
|
|
config *Config
|
|
}
|
|
|
|
var _ http.Handler = (*ZipkinReceiver)(nil)
|
|
|
|
// New creates a new zipkinreceiver.ZipkinReceiver reference.
|
|
func New(config *Config, nextConsumer consumer.Traces) (*ZipkinReceiver, error) {
|
|
if nextConsumer == nil {
|
|
return nil, componenterror.ErrNilNextConsumer
|
|
}
|
|
|
|
zr := &ZipkinReceiver{
|
|
nextConsumer: nextConsumer,
|
|
instanceName: config.Name(),
|
|
config: config,
|
|
}
|
|
return zr, nil
|
|
}
|
|
|
|
// Start spins up the receiver's HTTP server and makes the receiver start its processing.
|
|
func (zr *ZipkinReceiver) Start(ctx context.Context, host component.Host) error {
|
|
if host == nil {
|
|
return errors.New("nil host")
|
|
}
|
|
|
|
zr.mu.Lock()
|
|
defer zr.mu.Unlock()
|
|
|
|
var err = componenterror.ErrAlreadyStarted
|
|
|
|
zr.startOnce.Do(func() {
|
|
err = nil
|
|
zr.host = host
|
|
zr.server = zr.config.HTTPServerSettings.ToServer(zr)
|
|
var listener net.Listener
|
|
listener, err = zr.config.HTTPServerSettings.ToListener()
|
|
if err != nil {
|
|
return
|
|
}
|
|
zr.shutdownWG.Add(1)
|
|
go func() {
|
|
defer zr.shutdownWG.Done()
|
|
|
|
if errHTTP := zr.server.Serve(listener); errHTTP != http.ErrServerClosed {
|
|
host.ReportFatalError(errHTTP)
|
|
}
|
|
}()
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// v1ToTraceSpans parses Zipkin v1 JSON traces and converts them to OpenCensus Proto spans.
|
|
func (zr *ZipkinReceiver) v1ToTraceSpans(blob []byte, hdr http.Header) (reqs pdata.Traces, err error) {
|
|
if hdr.Get("Content-Type") == "application/x-thrift" {
|
|
zSpans, err := jaegerzipkin.DeserializeThrift(blob)
|
|
if err != nil {
|
|
return pdata.NewTraces(), err
|
|
}
|
|
|
|
return zipkin.V1ThriftBatchToInternalTraces(zSpans)
|
|
}
|
|
return zipkin.V1JSONBatchToInternalTraces(blob, zr.config.ParseStringTags)
|
|
}
|
|
|
|
// v2ToTraceSpans parses Zipkin v2 JSON or Protobuf traces and converts them to OpenCensus Proto spans.
|
|
func (zr *ZipkinReceiver) v2ToTraceSpans(blob []byte, hdr http.Header) (reqs pdata.Traces, err error) {
|
|
// This flag's reference is from:
|
|
// https://github.com/openzipkin/zipkin-go/blob/3793c981d4f621c0e3eb1457acffa2c1cc591384/proto/v2/zipkin.proto#L154
|
|
debugWasSet := hdr.Get("X-B3-Flags") == "1"
|
|
|
|
var zipkinSpans []*zipkinmodel.SpanModel
|
|
|
|
// Zipkin can send protobuf via http
|
|
switch hdr.Get("Content-Type") {
|
|
// TODO: (@odeke-em) record the unique types of Content-Type uploads
|
|
case "application/x-protobuf":
|
|
zipkinSpans, err = zipkin_proto3.ParseSpans(blob, debugWasSet)
|
|
|
|
default: // By default, we'll assume using JSON
|
|
zipkinSpans, err = zr.deserializeFromJSON(blob)
|
|
}
|
|
|
|
if err != nil {
|
|
return pdata.Traces{}, err
|
|
}
|
|
|
|
return zipkin.V2SpansToInternalTraces(zipkinSpans, zr.config.ParseStringTags)
|
|
}
|
|
|
|
func (zr *ZipkinReceiver) deserializeFromJSON(jsonBlob []byte) (zs []*zipkinmodel.SpanModel, err error) {
|
|
if err = json.Unmarshal(jsonBlob, &zs); err != nil {
|
|
return nil, err
|
|
}
|
|
return zs, nil
|
|
}
|
|
|
|
// Shutdown tells the receiver that should stop reception,
|
|
// giving it a chance to perform any necessary clean-up and shutting down
|
|
// its HTTP server.
|
|
func (zr *ZipkinReceiver) Shutdown(context.Context) error {
|
|
var err = componenterror.ErrAlreadyStopped
|
|
zr.stopOnce.Do(func() {
|
|
err = zr.server.Close()
|
|
zr.shutdownWG.Wait()
|
|
})
|
|
return err
|
|
}
|
|
|
|
// processBodyIfNecessary checks the "Content-Encoding" HTTP header and if
|
|
// a compression such as "gzip", "deflate", "zlib", is found, the body will
|
|
// be uncompressed accordingly or return the body untouched if otherwise.
|
|
// Clients such as Zipkin-Java do this behavior e.g.
|
|
// send "Content-Encoding":"gzip" of the JSON content.
|
|
func processBodyIfNecessary(req *http.Request) io.Reader {
|
|
switch req.Header.Get("Content-Encoding") {
|
|
default:
|
|
return req.Body
|
|
|
|
case "gzip":
|
|
return gunzippedBodyIfPossible(req.Body)
|
|
|
|
case "deflate", "zlib":
|
|
return zlibUncompressedbody(req.Body)
|
|
}
|
|
}
|
|
|
|
func gunzippedBodyIfPossible(r io.Reader) io.Reader {
|
|
gzr, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
// Just return the old body as was
|
|
return r
|
|
}
|
|
return gzr
|
|
}
|
|
|
|
func zlibUncompressedbody(r io.Reader) io.Reader {
|
|
zr, err := zlib.NewReader(r)
|
|
if err != nil {
|
|
// Just return the old body as was
|
|
return r
|
|
}
|
|
return zr
|
|
}
|
|
|
|
const (
|
|
zipkinV1TagValue = "zipkinV1"
|
|
zipkinV2TagValue = "zipkinV2"
|
|
)
|
|
|
|
// The ZipkinReceiver receives spans from endpoint /api/v2 as JSON,
|
|
// unmarshals them and sends them along to the nextConsumer.
|
|
func (zr *ZipkinReceiver) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
if c, ok := client.FromHTTP(r); ok {
|
|
ctx = client.NewContext(ctx, c)
|
|
}
|
|
|
|
// Now deserialize and process the spans.
|
|
asZipkinv1 := r.URL != nil && strings.Contains(r.URL.Path, "api/v1/spans")
|
|
|
|
transportTag := transportType(r)
|
|
ctx = obsreport.ReceiverContext(ctx, zr.instanceName, transportTag)
|
|
ctx = obsreport.StartTraceDataReceiveOp(ctx, zr.instanceName, transportTag)
|
|
|
|
pr := processBodyIfNecessary(r)
|
|
slurp, _ := ioutil.ReadAll(pr)
|
|
if c, ok := pr.(io.Closer); ok {
|
|
_ = c.Close()
|
|
}
|
|
_ = r.Body.Close()
|
|
|
|
var td pdata.Traces
|
|
var err error
|
|
if asZipkinv1 {
|
|
td, err = zr.v1ToTraceSpans(slurp, r.Header)
|
|
} else {
|
|
td, err = zr.v2ToTraceSpans(slurp, r.Header)
|
|
}
|
|
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
consumerErr := zr.nextConsumer.ConsumeTraces(ctx, td)
|
|
|
|
receiverTagValue := zipkinV2TagValue
|
|
if asZipkinv1 {
|
|
receiverTagValue = zipkinV1TagValue
|
|
}
|
|
obsreport.EndTraceDataReceiveOp(ctx, receiverTagValue, td.SpanCount(), consumerErr)
|
|
|
|
if consumerErr != nil {
|
|
// Transient error, due to some internal condition.
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write(errNextConsumerRespBody)
|
|
return
|
|
}
|
|
|
|
// Finally send back the response "Accepted" as
|
|
// required at https://zipkin.io/zipkin-api/#/default/post_spans
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
func transportType(r *http.Request) string {
|
|
v1 := r.URL != nil && strings.Contains(r.URL.Path, "api/v1/spans")
|
|
if v1 {
|
|
if r.Header.Get("Content-Type") == "application/x-thrift" {
|
|
return receiverTransportV1Thrift
|
|
}
|
|
return receiverTransportV1JSON
|
|
}
|
|
if r.Header.Get("Content-Type") == "application/x-protobuf" {
|
|
return receiverTransportV2PROTO
|
|
}
|
|
return receiverTransportV2JSON
|
|
}
|