diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..b0258d9 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,16 @@ +name: tests +on: [push, pull_request] +jobs: + tests: + strategy: + matrix: + go-version: [1.20.x, 1.21.x] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + name: "install go" + with: + go-version: ${{ matrix.go-version }} + - name: "tests" + run: make test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ba11d0a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: go - -go: - - 1.12.x - - 1.11.x - -go_import_path: github.com/zerofox-oss/go-msg diff --git a/decorators/otel/tracing/doc.go b/decorators/otel/tracing/doc.go new file mode 100644 index 0000000..4c0fd80 --- /dev/null +++ b/decorators/otel/tracing/doc.go @@ -0,0 +1,38 @@ +// Tracing provides decorators which enable distributed tracing +// +// How it works +// +// This package provides two decorators which can be used to +// propagate tracing information. The topic decorator "tracing.Topic" +// will automatically attach tracing information to any outgoing +// messages. If no parent trace exists, it will create one automatically. +// The second decorator, tracing.Receiver is used to decode tracing information +// into the context.Context object which is passed into the receiver that you +// provide handle messages. Again if to trace is present a trace is started and +// set in the context. +// +// Examples +// +// Using the tracing.Topic: +// +// func ExampleTopic() { +// // make a concrete topic eg SNS +// topic, _ := sns.NewTopic("arn://sns:xxx") +// // make a tracing topic with the span name "msg.Writer" +// topic := tracing.TracingTopic(topic, tracing.WithSpanName("msg.Writer")) +// // use topic as you would without tracing +// } +// +// Using the tracing.Receiver: +// +// func ExampleReceiver() { +// receiver := msg.Receiver(func(ctx context.Context, m *msg.Message) error { +// // your receiver implementation +// // ctx will contain tracing information +// // once decorated +// }) +// receiver := tracing.Receiver(receiver) +// // use receiver as you would without tracing +// } +// +package tracing diff --git a/decorators/otel/tracing/receiver.go b/decorators/otel/tracing/receiver.go new file mode 100644 index 0000000..9db909a --- /dev/null +++ b/decorators/otel/tracing/receiver.go @@ -0,0 +1,123 @@ +package tracing + +import ( + "context" + "encoding/base64" + + "github.com/zerofox-oss/go-msg" + "go.opencensus.io/trace/propagation" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + + ocbridge "go.opentelemetry.io/otel/bridge/opencensus" +) + +var tracer = otel.Tracer("github.com/zerofox-oss/go-msg/decorators/otel") + +const traceContextKey = "Tracecontext" +const traceStateKey = "Tracestate" + +type Options struct { + SpanName string + StartOptions trace.SpanStartOption + OnlyOtel bool +} + +type Option func(*Options) + +func WithSpanName(spanName string) Option { + return func(o *Options) { + o.SpanName = spanName + } +} + +func WithStartOption(so trace.SpanStartOption) Option { + return func(o *Options) { + o.StartOptions = so + } +} + +func WithOnlyOtel(onlyOtel bool) Option { + return func(o *Options) { + o.OnlyOtel = onlyOtel + } +} + +// Receiver Wraps another msg.Receiver, populating +// the context with any upstream tracing information. +func Receiver(next msg.Receiver, opts ...Option) msg.Receiver { + + options := &Options{ + SpanName: "msg.Receiver", + } + + for _, opt := range opts { + opt(options) + } + + return msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { + ctx, span := withContext(ctx, m, options) + defer span.End() + return next.Receive(ctx, m) + }) +} + +// withContext checks to see if a traceContext is +// present in the message attributes. If one is present +// a new span is created with that tracecontext as the parent +// otherwise a new span is created without a parent. A new context +// which contains the created span well as the span itself +// is returned +func withContext(ctx context.Context, m *msg.Message, options *Options) (context.Context, trace.Span) { + + textCarrier := msgAttributesTextCarrier{attributes: &m.Attributes} + tmprop := otel.GetTextMapPropagator() + + // if any of the fields used by + // the text map propagation is set + // we use otel to decode + for _, field := range tmprop.Fields() { + if m.Attributes.Get(field) != "" { + ctx = tmprop.Extract(ctx, textCarrier) + return tracer.Start(ctx, options.SpanName) + } + } + + // if we are set to use only otel + // do not fall back to opencensus + if options.OnlyOtel { + return tracer.Start(ctx, options.SpanName) + } + + // fallback to old behaviour (opencensus) if we don't + // receive any otel headers + traceContextB64 := m.Attributes.Get(traceContextKey) + if traceContextB64 == "" { + return tracer.Start(ctx, options.SpanName) + } + + traceContext, err := base64.StdEncoding.DecodeString(traceContextB64) + if err != nil { + return tracer.Start(ctx, options.SpanName) + } + + spanContext, ok := propagation.FromBinary(traceContext) + if !ok { + return tracer.Start(ctx, options.SpanName) + } + + traceStateString := m.Attributes.Get(traceStateKey) + if traceStateString != "" { + ts := tracestateFromString(traceStateString) + spanContext.Tracestate = ts + } + + // convert the opencensus span context to otel + otelSpanContext := ocbridge.OCSpanContextToOTel(spanContext) + if !otelSpanContext.IsValid() { + return tracer.Start(ctx, options.SpanName) + } + + return tracer.Start(trace.ContextWithRemoteSpanContext(ctx, otelSpanContext), options.SpanName) +} diff --git a/decorators/otel/tracing/receiver_test.go b/decorators/otel/tracing/receiver_test.go new file mode 100644 index 0000000..a68b286 --- /dev/null +++ b/decorators/otel/tracing/receiver_test.go @@ -0,0 +1,246 @@ +package tracing + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "io/ioutil" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zerofox-oss/go-msg" + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" + + ocbridge "go.opentelemetry.io/otel/bridge/opencensus" + oteltrace "go.opentelemetry.io/otel/trace" +) + +type msgWithContext struct { + msg *msg.Message + ctx context.Context +} + +type ChanReceiver struct { + c chan msgWithContext +} + +func (r ChanReceiver) Receive(ctx context.Context, m *msg.Message) error { + r.c <- msgWithContext{msg: m, ctx: ctx} + return nil +} + +func makeOCSpanContext() (trace.SpanContext, string) { + b := make([]byte, 24) + rand.Read(b) + + var tid [16]byte + var sid [8]byte + + copy(tid[:], b[:16]) + copy(sid[:], b[:8]) + + sc := trace.SpanContext{ + TraceID: tid, + SpanID: sid, + } + + b64 := base64.StdEncoding.EncodeToString(propagation.Binary(sc)) + return sc, b64 +} + +// Tests that when a Receiver is wrapped by TracingReceiver, and tracecontext +// is present, a OC span is started is started and set in the receive context with the correct +// parent context +func TestDecoder_SuccessfullyDecodesSpanWhenTraceContextIsPresent(t *testing.T) { + testFinish := make(chan struct{}) + msgChan := make(chan msgWithContext) + r := Receiver(ChanReceiver{ + c: msgChan, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sc, b64Sc := makeOCSpanContext() + + // Construct a message with base64 encoding (YWJjMTIz == abc123) + m := &msg.Message{ + Body: bytes.NewBufferString("hello"), + Attributes: msg.Attributes{}, + } + m.Attributes.Set("Tracecontext", b64Sc) + + // Wait for ChanReceiver to write the message to msgChan, assert on the body + go func() { + result := <-msgChan + + expectedBody := "hello" + actual, _ := ioutil.ReadAll(result.msg.Body) + if string(actual) != expectedBody { + t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) + } + + span := oteltrace.SpanFromContext(result.ctx) + if span == nil { + t.Errorf("span was not expected to be nil") + } + + receivedSC := span.SpanContext() + ocReceivedSC := ocbridge.OTelSpanContextToOC(receivedSC) + + if ocReceivedSC.TraceID != sc.TraceID { + t.Errorf(cmp.Diff(receivedSC.TraceID, sc.TraceID)) + } + + testFinish <- struct{}{} + }() + + // Receive the message! + err := r.Receive(ctx, m) + if err != nil { + t.Error(err) + return + } + <-testFinish +} + +// Tests that when a Receiver is wrapped by a Tracing Receiver, and +// the message does not contain a tracecontext, a new span is created +func TestDecoder_SuccessfullySetsSpanWhenNoTraceContext(t *testing.T) { + testFinish := make(chan struct{}) + msgChan := make(chan msgWithContext) + r := Receiver(ChanReceiver{ + c: msgChan, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Construct a message without base64 encoding + m := &msg.Message{ + Body: bytes.NewBufferString("abc123"), + Attributes: msg.Attributes{}, + } + + // Wait for ChanReceiver to write the message to msgChan, assert on the body + go func() { + result := <-msgChan + expectedBody := "abc123" + actual, _ := ioutil.ReadAll(result.msg.Body) + if string(actual) != expectedBody { + t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) + } + + span := oteltrace.SpanFromContext(result.ctx) + if span == nil { + t.Errorf("span was not expected to be nil") + } + + testFinish <- struct{}{} + }() + + // Receive the message! + err := r.Receive(ctx, m) + if err != nil { + t.Error(err) + return + } + <-testFinish +} + +// Tests that when a Receiver is wrapped by a Tracing Receiver, and +// the message contains an invalid b64 encodeded tracecontext, a span +// is still sucessfully set +func TestDecoder_SuccessfullySetsSpanWhenInvalidTraceContextB64(t *testing.T) { + testFinish := make(chan struct{}) + msgChan := make(chan msgWithContext) + r := Receiver(ChanReceiver{ + c: msgChan, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Construct a message without base64 encoding + m := &msg.Message{ + Body: bytes.NewBufferString("abc123"), + Attributes: msg.Attributes{}, + } + + m.Attributes.Set("Tracecontext", "invalidcontext") + + // Wait for ChanReceiver to write the message to msgChan, assert on the body + go func() { + result := <-msgChan + expectedBody := "abc123" + actual, _ := ioutil.ReadAll(result.msg.Body) + if string(actual) != expectedBody { + t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) + } + + span := oteltrace.SpanFromContext(result.ctx) + if span == nil { + t.Errorf("span was not expected to be nil") + } + + testFinish <- struct{}{} + }() + + // Receive the message! + err := r.Receive(ctx, m) + if err != nil { + t.Error(err) + return + } + <-testFinish +} + +// Tests that when a Receiver is wrapped by a Tracing Receiver, and +// the message contains an invalid binary encodeded tracecontext, a span +// is still sucessfully set +func TestDecoder_SuccessfullySetsSpanWhenInvalidTraceContextBinary(t *testing.T) { + testFinish := make(chan struct{}) + msgChan := make(chan msgWithContext) + r := Receiver(ChanReceiver{ + c: msgChan, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Construct a message without base64 encoding + m := &msg.Message{ + Body: bytes.NewBufferString("abc123"), + Attributes: msg.Attributes{}, + } + + // "YWJjMTIz" is valid b64 + m.Attributes.Set("Tracecontext", "YWJjMTIz") + + // Wait for ChanReceiver to write the message to msgChan, assert on the body + go func() { + result := <-msgChan + expectedBody := "abc123" + actual, _ := ioutil.ReadAll(result.msg.Body) + if string(actual) != expectedBody { + t.Errorf("Expected Body to be %v, got %v", expectedBody, string(actual)) + } + + span := oteltrace.SpanFromContext(result.ctx) + if span == nil { + t.Errorf("span was not expected to be nil") + } + + testFinish <- struct{}{} + }() + + // Receive the message! + err := r.Receive(ctx, m) + if err != nil { + t.Error(err) + return + } + <-testFinish +} diff --git a/decorators/otel/tracing/topic.go b/decorators/otel/tracing/topic.go new file mode 100644 index 0000000..5c5e3c0 --- /dev/null +++ b/decorators/otel/tracing/topic.go @@ -0,0 +1,157 @@ +package tracing + +import ( + "bytes" + "context" + "encoding/base64" + "strings" + "sync" + + "github.com/zerofox-oss/go-msg" + "go.opencensus.io/trace/propagation" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + ocbridge "go.opentelemetry.io/otel/bridge/opencensus" + "go.opentelemetry.io/otel/trace" +) + +func msgAttributesToTrace(msgAttributes msg.Attributes) []attribute.KeyValue { + traceAttributes := make([]attribute.KeyValue, len(msgAttributes)) + for key, value := range msgAttributes { + traceAttributes = append(traceAttributes, attribute.String(key, strings.Join(value, ";"))) + } + return traceAttributes +} + +type msgAttributesTextCarrier struct { + attributes *msg.Attributes +} + +func (m msgAttributesTextCarrier) Get(key string) string { + return m.attributes.Get(key) +} + +func (m msgAttributesTextCarrier) Set(key string, value string) { + m.attributes.Set(key, value) +} + +func (m msgAttributesTextCarrier) Keys() []string { + keys := []string{} + for key := range *m.attributes { + keys = append(keys, key) + } + return keys +} + +// Topic wraps a msg.Topic, attaching any tracing data +// via msg.Attributes to send downstream +func Topic(next msg.Topic, opts ...Option) msg.Topic { + + options := &Options{ + SpanName: "msg.MessageWriter", + } + + for _, opt := range opts { + opt(options) + } + + return msg.TopicFunc(func(ctx context.Context) msg.MessageWriter { + tracingCtx, span := tracer.Start( + ctx, + options.SpanName, + trace.WithSpanKind(trace.SpanKindProducer), + ) + + return &tracingWriter{ + Next: next.NewWriter(tracingCtx), + ctx: tracingCtx, + onClose: span.End, + options: options, + } + }) +} + +type tracingWriter struct { + Next msg.MessageWriter + + buf bytes.Buffer + closed bool + mux sync.Mutex + ctx context.Context + + // onClose is used to end the span + // and send data to tracing framework + onClose func(...trace.SpanEndOption) + + options *Options +} + +// Attributes returns the attributes associated with the MessageWriter. +func (w *tracingWriter) Attributes() *msg.Attributes { + return w.Next.Attributes() +} + +// Close adds tracing message attributes +// writing to the next MessageWriter. +func (w *tracingWriter) Close() error { + w.mux.Lock() + defer w.mux.Unlock() + defer w.onClose() + + if w.closed { + return msg.ErrClosedMessageWriter + } + w.closed = true + + dataToWrite := w.buf.Bytes() + + if span := trace.SpanFromContext(w.ctx); span != nil { + // set message attributes + // as span tags for debugging + attrs := *w.Attributes() + span.SetAttributes(msgAttributesToTrace(attrs)...) + + // we use the global text map propagator + // to set string values onto the message + // attributes, this will set the headers + // in the new style tracecontext format + textCarrier := msgAttributesTextCarrier{attributes: w.Attributes()} + tmprop := otel.GetTextMapPropagator() + tmprop.Inject(w.ctx, textCarrier) + + // also send opencensus headers + // for backwards compatiblity + if !w.options.OnlyOtel { + // we need to convert the otel + // span to the old style opencensus + sc := span.SpanContext() + ocSpan := ocbridge.OTelSpanContextToOC(sc) + bs := propagation.Binary(ocSpan) + traceBuf := make([]byte, base64.StdEncoding.EncodedLen(len(bs))) + base64.StdEncoding.Encode(traceBuf, bs) + + attrs.Set(traceContextKey, string(traceBuf)) + tracestateString := tracestateToString(ocSpan) + if tracestateString != "" { + attrs.Set(traceStateKey, tracestateToString(ocSpan)) + } + } + } + + if _, err := w.Next.Write(dataToWrite); err != nil { + return err + } + return w.Next.Close() +} + +// Write writes bytes to an internal buffer. +func (w *tracingWriter) Write(b []byte) (int, error) { + w.mux.Lock() + defer w.mux.Unlock() + + if w.closed { + return 0, msg.ErrClosedMessageWriter + } + return w.buf.Write(b) +} diff --git a/decorators/otel/tracing/topic_test.go b/decorators/otel/tracing/topic_test.go new file mode 100644 index 0000000..7a36a3c --- /dev/null +++ b/decorators/otel/tracing/topic_test.go @@ -0,0 +1,108 @@ +package tracing + +import ( + "context" + "encoding/base64" + "testing" + + msg "github.com/zerofox-oss/go-msg" + "github.com/zerofox-oss/go-msg/backends/mem" + ocprop "go.opencensus.io/trace/propagation" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +func TestTopic__SucessfullyInsertsTraceContext(t *testing.T) { + + tp := tracesdk.NewTracerProvider() + otel.SetTextMapPropagator(propagation.TraceContext{}) + otel.SetTracerProvider(tp) + + t.Cleanup(func() { + tp.Shutdown(context.Background()) + }) + c := make(chan *msg.Message, 2) + + // setup topics + t2 := Topic(&mem.Topic{C: c}, WithSpanName("something.Different")) + + w := t2.NewWriter(context.Background()) + w.Write([]byte("hello,")) + w.Write([]byte("world!")) + w.Close() + + m := <-c + body, err := msg.DumpBody(m) + if err != nil { + t.Fatal(err) + } + + expectedBody := "hello,world!" + if string(body) != expectedBody { + t.Fatalf("got %s expected %s", string(body), expectedBody) + } + + tc := m.Attributes.Get("Tracecontext") + if tc == "" { + t.Fatalf("expected tracecontext attribute to be set") + } + + b, err := base64.StdEncoding.DecodeString(tc) + if err != nil { + t.Error(err) + } + + _, ok := ocprop.FromBinary(b) + if !ok { + t.Errorf("expected spanContext to be decoded from tracecontext attribute") + } +} + +func TestTopic__SucessfullyInsertsOtelContext(t *testing.T) { + + tp := tracesdk.NewTracerProvider() + otel.SetTextMapPropagator(propagation.TraceContext{}) + otel.SetTracerProvider(tp) + + t.Cleanup(func() { + tp.Shutdown(context.Background()) + }) + c := make(chan *msg.Message, 2) + + // setup topics + t2 := Topic(&mem.Topic{C: c}, WithSpanName("something.Different")) + + w := t2.NewWriter(context.Background()) + w.Write([]byte("hello,")) + w.Write([]byte("world!")) + w.Close() + + m := <-c + body, err := msg.DumpBody(m) + if err != nil { + t.Fatal(err) + } + + expectedBody := "hello,world!" + if string(body) != expectedBody { + t.Fatalf("got %s expected %s", string(body), expectedBody) + } + + traceparent := m.Attributes.Get("Traceparent") + if traceparent == "" { + t.Fatalf("expected traceparent attribute to be set") + } + + textMapProp := otel.GetTextMapPropagator() + ctx := textMapProp.Extract(context.Background(), msgAttributesTextCarrier{attributes: &m.Attributes}) + span := trace.SpanFromContext(ctx) + if !span.SpanContext().IsValid() { + t.Fatalf("expected otel span to be valid") + } + + if !span.SpanContext().IsRemote() { + t.Fatalf("expected otel span to be remote") + } +} diff --git a/decorators/otel/tracing/tracing_test.go b/decorators/otel/tracing/tracing_test.go new file mode 100644 index 0000000..dd066e8 --- /dev/null +++ b/decorators/otel/tracing/tracing_test.go @@ -0,0 +1,165 @@ +package tracing_test + +import ( + "context" + "sync" + "testing" + + "github.com/zerofox-oss/go-msg" + octrace "go.opencensus.io/trace" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + + "github.com/zerofox-oss/go-msg/backends/mem" + "github.com/zerofox-oss/go-msg/decorators/otel/tracing" + octracing "github.com/zerofox-oss/go-msg/decorators/tracing" +) + +type message struct { + ctx context.Context + m *msg.Message +} + +func startOTSpan() (context.Context, string, func()) { + ctx := context.Background() + ctx, span := otel.Tracer("tracing_test").Start(ctx, "WriteMsg") + return ctx, span.SpanContext().TraceID().String(), func() { + span.End() + } +} + +func startOCSpan() (context.Context, string, func()) { + ctx := context.Background() + ctx, span := octrace.StartSpan(ctx, "WriteMsg") + return ctx, span.SpanContext().TraceID.String(), func() { + span.End() + } +} + +func readOCSpan(ctx context.Context) string { + span := octrace.FromContext(ctx) + if span != nil { + return span.SpanContext().TraceID.String() + } + return "" +} + +func readOTSpan(ctx context.Context) string { + receivedSpan := trace.SpanFromContext(ctx) + receivedSpanContext := receivedSpan.SpanContext() + return receivedSpanContext.TraceID().String() +} + +func verifyEncodeDecode( + t *testing.T, + topicDecorator func(next msg.Topic) msg.Topic, + receiveDecorator func(next msg.Receiver) msg.Receiver, + spanCreator func() (context.Context, string, func()), + spanReader func(context.Context) string, +) { + + c1 := make(chan *msg.Message) + topic := mem.Topic{C: c1} + srv := mem.NewServer(c1, 1) + + var lock sync.RWMutex + + receivedMessages := []message{} + go func() { + srv.Serve(receiveDecorator(msg.ReceiverFunc(func(ctx context.Context, m *msg.Message) error { + lock.Lock() + receivedMessages = append(receivedMessages, message{ctx: ctx, m: m}) + lock.Unlock() + return nil + }))) + }() + + expectedTraces := []string{} + for i := 0; i < 20; i++ { + ctx, traceID, end := spanCreator() + writer := topicDecorator(&topic).NewWriter(ctx) + writer.Write([]byte("message body")) + writer.Close() + end() + expectedTraces = append(expectedTraces, traceID) + } + + srv.Shutdown(context.Background()) + + for i, expectedTraceID := range expectedTraces { + lock.RLock() + m := receivedMessages[i] + lock.RUnlock() + traceID := spanReader(m.ctx) + if traceID != expectedTraceID { + t.Errorf("received span did not have the expected id %s != %s", traceID, expectedTraceID) + } + } + +} + +func TestTopicAndReceiverCompatability(t *testing.T) { + + tp := tracesdk.NewTracerProvider() + + otel.SetTextMapPropagator(propagation.TraceContext{}) + otel.SetTracerProvider(tp) + + t.Cleanup(func() { + tp.Shutdown(context.Background()) + }) + + t.Run("Otel->Otel WithOnlyOtel=true", func(t *testing.T) { + verifyEncodeDecode(t, + func(next msg.Topic) msg.Topic { + return tracing.Topic(next, tracing.WithOnlyOtel(true)) + }, + func(next msg.Receiver) msg.Receiver { + return tracing.Receiver(next, tracing.WithOnlyOtel(true)) + }, + startOTSpan, + readOTSpan, + ) + }) + + t.Run("Otel->Otel WithOnlyOtel=false", func(t *testing.T) { + verifyEncodeDecode(t, + func(next msg.Topic) msg.Topic { + return tracing.Topic(next, tracing.WithOnlyOtel(false)) + }, + func(next msg.Receiver) msg.Receiver { + return tracing.Receiver(next, tracing.WithOnlyOtel(false)) + }, + startOTSpan, + readOTSpan, + ) + }) + + t.Run("OC->Otel", func(t *testing.T) { + verifyEncodeDecode(t, + func(next msg.Topic) msg.Topic { + return octracing.Topic(next) + }, + func(next msg.Receiver) msg.Receiver { + return tracing.Receiver(next, tracing.WithOnlyOtel(false)) + }, + startOCSpan, + readOTSpan, + ) + }) + + t.Run("Otel->OC", func(t *testing.T) { + verifyEncodeDecode(t, + func(next msg.Topic) msg.Topic { + return tracing.Topic(next, tracing.WithOnlyOtel(false)) + }, + func(next msg.Receiver) msg.Receiver { + return octracing.Receiver(next) + }, + startOTSpan, + readOCSpan, + ) + }) +} diff --git a/decorators/otel/tracing/utils.go b/decorators/otel/tracing/utils.go new file mode 100644 index 0000000..292fe67 --- /dev/null +++ b/decorators/otel/tracing/utils.go @@ -0,0 +1,59 @@ +package tracing + +import ( + "regexp" + "strings" + + "go.opencensus.io/trace" + "go.opencensus.io/trace/tracestate" +) + +// CODE BASED ON: +// https://github.com/census-instrumentation/opencensus-go/blob/ \ +// master/plugin/ochttp/propagation/tracecontext/propagation.go + +const ( + trimOWSRegexFmt = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$` + maxTracestateLen = 512 +) + +var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt) // nolint + +func tracestateToString(sc trace.SpanContext) string { + var pairs = make([]string, 0, len(sc.Tracestate.Entries())) + if sc.Tracestate != nil { + for _, entry := range sc.Tracestate.Entries() { + pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "=")) + } + return strings.Join(pairs, ",") + } + return "" +} + +func tracestateFromString(tracestateString string) *tracestate.Tracestate { + var entries []tracestate.Entry // nolint + pairs := strings.Split(tracestateString, ",") + hdrLenWithoutOWS := len(pairs) - 1 // Number of commas + for _, pair := range pairs { + matches := trimOWSRegExp.FindStringSubmatch(pair) + if matches == nil { + return nil + } + pair = matches[1] + hdrLenWithoutOWS += len(pair) + if hdrLenWithoutOWS > maxTracestateLen { + return nil + } + kv := strings.Split(pair, "=") + if len(kv) != 2 { + return nil + } + entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]}) + } + ts, err := tracestate.New(nil, entries...) + if err != nil { + return nil + } + + return ts +} diff --git a/go.mod b/go.mod index b5e1e7a..b118f4f 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,22 @@ module github.com/zerofox-oss/go-msg -go 1.12 +go 1.20 require ( - github.com/google/go-cmp v0.3.0 + github.com/google/go-cmp v0.5.9 github.com/pierrec/lz4/v4 v4.1.8 - go.opencensus.io v0.22.0 + go.opencensus.io v0.24.0 + go.opentelemetry.io/otel v1.19.0 + go.opentelemetry.io/otel/bridge/opencensus v0.42.0 + go.opentelemetry.io/otel/sdk v1.19.0 + go.opentelemetry.io/otel/trace v1.19.0 +) + +require ( + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.19.0 // indirect + golang.org/x/sys v0.12.0 // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..37ebfd8 --- /dev/null +++ b/go.sum @@ -0,0 +1,120 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4= +github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/bridge/opencensus v0.42.0 h1:QvC+bcZkWMphWPiVqRQygMj6M0/3TOuJEO+erRA7kI8= +go.opentelemetry.io/otel/bridge/opencensus v0.42.0/go.mod h1:XJojP7g5DqYdiyArix/H9i1XzPPlIUc9dGLKtF9copI= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k= +go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=