From 84341a30129d925feb38eb8a6d126b36ef8d9afe Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 21 Apr 2020 14:16:24 +0200 Subject: [PATCH 1/4] Add an updated copy of otel-go mock tracer --- go.mod | 6 ++ go.sum | 82 +++++++++++++++++ internal/trace/mock_span.go | 96 +++++++++++++++++++ internal/trace/mock_tracer.go | 168 ++++++++++++++++++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 go.sum create mode 100644 internal/trace/mock_span.go create mode 100644 internal/trace/mock_tracer.go diff --git a/go.mod b/go.mod index 3a96b217dec..0a29ec7bd28 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module go.opentelemetry.io/contrib go 1.14 + +require ( + go.opentelemetry.io/otel v0.4.2 + google.golang.org/grpc v1.28.1 + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000000..37b25cabce5 --- /dev/null +++ b/go.sum @@ -0,0 +1,82 @@ +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/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= +github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +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/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.opentelemetry.io/otel v0.4.2 h1:nT+GOqqRR1cIY92xmo1DeiXLHtIlXH1KLRgnsnhuNrs= +go.opentelemetry.io/otel v0.4.2/go.mod h1:OgNpQOjrlt33Ew6Ds0mGjmcTQg/rhUctsbkRdk/g1fw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +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-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +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.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.1 h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k= +google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= diff --git a/internal/trace/mock_span.go b/internal/trace/mock_span.go new file mode 100644 index 00000000000..0a225211f7f --- /dev/null +++ b/internal/trace/mock_span.go @@ -0,0 +1,96 @@ +// 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 trace + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + + otelcore "go.opentelemetry.io/otel/api/core" + oteltrace "go.opentelemetry.io/otel/api/trace" +) + +// Span is a mock span used in association with Tracer for +// testing purpose only. +type Span struct { + sc otelcore.SpanContext + tracer *Tracer + Name string + Attributes map[otelcore.Key]otelcore.Value + Kind oteltrace.SpanKind + Status codes.Code + ParentSpanID otelcore.SpanID +} + +var _ oteltrace.Span = (*Span)(nil) + +// SpanContext returns associated otelcore.SpanContext. +// +// If the receiver is nil it returns an empty otelcore.SpanContext. +func (ms *Span) SpanContext() otelcore.SpanContext { + if ms == nil { + return otelcore.EmptySpanContext() + } + return ms.sc +} + +// IsRecording always returns false for Span. +func (ms *Span) IsRecording() bool { + return false +} + +// SetStatus sets the Status member. +func (ms *Span) SetStatus(status codes.Code, msg string) { + ms.Status = status +} + +// SetAttributes adds an attribute to Attributes member. +func (ms *Span) SetAttributes(attributes ...otelcore.KeyValue) { + if ms.Attributes == nil { + ms.Attributes = make(map[otelcore.Key]otelcore.Value) + } + for _, kv := range attributes { + ms.Attributes[kv.Key] = kv.Value + } +} + +// End puts the span into tracers ended spans. +func (ms *Span) End(options ...oteltrace.EndOption) { + ms.tracer.addEndedSpan(ms) +} + +// RecordError does nothing. +func (ms *Span) RecordError(ctx context.Context, err error, opts ...oteltrace.ErrorOption) { +} + +// SetName sets the span name. +func (ms *Span) SetName(name string) { + ms.Name = name +} + +// Tracer returns the mock tracer implementation of Tracer. +func (ms *Span) Tracer() oteltrace.Tracer { + return ms.tracer +} + +// AddEvent does nothing. +func (ms *Span) AddEvent(ctx context.Context, name string, attrs ...otelcore.KeyValue) { +} + +// AddEvent does nothing. +func (ms *Span) AddEventWithTimestamp(ctx context.Context, timestamp time.Time, name string, attrs ...otelcore.KeyValue) { +} diff --git a/internal/trace/mock_tracer.go b/internal/trace/mock_tracer.go new file mode 100644 index 00000000000..096c9e17eca --- /dev/null +++ b/internal/trace/mock_tracer.go @@ -0,0 +1,168 @@ +// 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 trace + +import ( + "context" + "crypto/rand" + "encoding/binary" + "sync" + "sync/atomic" + + otelcore "go.opentelemetry.io/otel/api/core" + oteltrace "go.opentelemetry.io/otel/api/trace" +) + +type Provider struct { + tracersLock sync.Mutex + tracers map[string]*Tracer +} + +var _ oteltrace.Provider = &Provider{} + +func (p *Provider) Tracer(name string) oteltrace.Tracer { + p.tracersLock.Lock() + defer p.tracersLock.Unlock() + if p.tracers == nil { + p.tracers = make(map[string]*Tracer) + } + if tracer, ok := p.tracers[name]; ok { + return tracer + } + tracer := NewTracer(name) + p.tracers[name] = tracer + return tracer +} + +// Tracer is a simple tracer used for testing purpose only. +// SpanID is atomically increased every time a new span is created. +type Tracer struct { + // StartSpanID is used to initialize span ID. It is incremented + // by one every time a new span is created. + // + // StartSpanID has to be aligned for 64-bit atomic operations. + StartSpanID uint64 + + // Name of the tracer, received from the provider. + Name string + + // Sampled specifies if the new span should be sampled or not. + Sampled bool + + // OnSpanStarted is called every time a new span is started. + OnSpanStarted func(span *Span) + + endedSpansLock sync.Mutex + endedSpans []*Span +} + +var _ oteltrace.Tracer = (*Tracer)(nil) + +func NewTracer(name string) *Tracer { + return &Tracer{ + Name: name, + } +} + +func (mt *Tracer) EndedSpans() []*Span { + var endedSpans []*Span + + mt.endedSpansLock.Lock() + endedSpans, mt.endedSpans = mt.endedSpans, nil + mt.endedSpansLock.Unlock() + + return endedSpans +} + +func (mt *Tracer) addEndedSpan(span *Span) { + mt.endedSpansLock.Lock() + mt.endedSpans = append(mt.endedSpans, span) + mt.endedSpansLock.Unlock() +} + +// WithSpan does nothing except creating a new span and executing the +// body. +func (mt *Tracer) WithSpan(ctx context.Context, name string, body func(context.Context) error, opts ...oteltrace.StartOption) error { + ctx, span := mt.Start(ctx, name, opts...) + defer span.End() + + return body(ctx) +} + +// Start starts a new Span and puts it into the context. +// +// The function generates a new random TraceID if either there is no +// parent SpanContext in context or the WithNewRoot option is passed +// to the function. Otherwise the function will take the TraceID from +// parent SpanContext. +// +// Currently no other StartOption has any effect here. +func (mt *Tracer) Start(ctx context.Context, name string, o ...oteltrace.StartOption) (context.Context, oteltrace.Span) { + var opts oteltrace.StartConfig + for _, op := range o { + op(&opts) + } + var span *Span + var sc otelcore.SpanContext + + parentSpanContext := getSpanContext(ctx, opts.NewRoot) + parentSpanID := parentSpanContext.SpanID + + if !parentSpanContext.IsValid() { + sc = otelcore.SpanContext{} + _, _ = rand.Read(sc.TraceID[:]) + if mt.Sampled { + sc.TraceFlags = otelcore.TraceFlagsSampled + } + } else { + sc = parentSpanContext + } + + binary.BigEndian.PutUint64(sc.SpanID[:], atomic.AddUint64(&mt.StartSpanID, 1)) + span = &Span{ + sc: sc, + tracer: mt, + Name: name, + Attributes: nil, + ParentSpanID: parentSpanID, + } + if len(opts.Attributes) > 0 { + span.SetAttributes(opts.Attributes...) + } + span.Kind = opts.SpanKind + if mt.OnSpanStarted != nil { + mt.OnSpanStarted(span) + } + + return oteltrace.ContextWithSpan(ctx, span), span +} + +func getSpanContext(ctx context.Context, ignoreContext bool) otelcore.SpanContext { + if ignoreContext { + return otelcore.EmptySpanContext() + } + + lsctx := oteltrace.SpanFromContext(ctx).SpanContext() + if lsctx.IsValid() { + return lsctx + } + + rsctx := oteltrace.RemoteSpanContextFromContext(ctx) + if rsctx.IsValid() { + return rsctx + } + + return otelcore.EmptySpanContext() +} From b6a963b76fd8b95d2b91e7731126e096af7efc6b Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 22 Apr 2020 15:46:10 +0200 Subject: [PATCH 2/4] Add functions for getting attributes from HTTP requests --- go.mod | 1 + internal/trace/http.go | 277 +++++++++++++ internal/trace/http_test.go | 778 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1056 insertions(+) create mode 100644 internal/trace/http.go create mode 100644 internal/trace/http_test.go diff --git a/go.mod b/go.mod index 0a29ec7bd28..58c8c1713d8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module go.opentelemetry.io/contrib go 1.14 require ( + github.com/stretchr/testify v1.4.0 go.opentelemetry.io/otel v0.4.2 google.golang.org/grpc v1.28.1 gopkg.in/yaml.v2 v2.2.8 // indirect diff --git a/internal/trace/http.go b/internal/trace/http.go new file mode 100644 index 00000000000..225bead2210 --- /dev/null +++ b/internal/trace/http.go @@ -0,0 +1,277 @@ +// 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 trace + +import ( + "fmt" + "net" + "net/http" + "strconv" + "strings" + + "google.golang.org/grpc/codes" + + otelcore "go.opentelemetry.io/otel/api/core" + otelkey "go.opentelemetry.io/otel/api/key" +) + +// NetAttributesFromHTTPRequest generates attributes of the net +// namespace as specified by the OpenTelemetry specification for a +// span. The network parameter is a string that net.Dial function +// from standard library can understand. +func NetAttributesFromHTTPRequest(network string, request *http.Request) []otelcore.KeyValue { + transport := "" + switch network { + case "tcp", "tcp4", "tcp6": + transport = "IP.TCP" + case "udp", "udp4", "udp6": + transport = "IP.UDP" + case "ip", "ip4", "ip6": + transport = "IP" + case "unix", "unixgram", "unixpacket": + transport = "Unix" + default: + transport = "other" + } + attrs := []otelcore.KeyValue{ + otelkey.String("net.transport", transport), + } + + peerName, peerIP, peerPort := "", "", 0 + { + hostPart := request.RemoteAddr + portPart := "" + if idx := strings.LastIndex(hostPart, ":"); idx >= 0 { + hostPart = request.RemoteAddr[:idx] + portPart = request.RemoteAddr[idx+1:] + } + if hostPart != "" { + if ip := net.ParseIP(hostPart); ip != nil { + peerIP = ip.String() + } else { + peerName = hostPart + } + + if portPart != "" { + numPort, err := strconv.ParseUint(portPart, 10, 16) + if err == nil { + peerPort = (int)(numPort) + } else { + peerName, peerIP = "", "" + } + } + } + } + if peerName != "" { + attrs = append(attrs, otelkey.String("net.peer.name", peerName)) + } + if peerIP != "" { + attrs = append(attrs, otelkey.String("net.peer.ip", peerIP)) + } + if peerPort != 0 { + attrs = append(attrs, otelkey.Int("net.peer.port", peerPort)) + } + hostIP, hostName, hostPort := "", "", 0 + for _, someHost := range []string{request.Host, request.Header.Get("Host"), request.URL.Host} { + hostPart := "" + if idx := strings.LastIndex(someHost, ":"); idx >= 0 { + strPort := someHost[idx+1:] + numPort, err := strconv.ParseUint(strPort, 10, 16) + if err == nil { + hostPort = (int)(numPort) + } + hostPart = someHost[:idx] + } else { + hostPart = someHost + } + if hostPart != "" { + ip := net.ParseIP(hostPart) + if ip != nil { + hostIP = ip.String() + } else { + hostName = hostPart + } + break + } else { + hostPort = 0 + } + } + if hostIP != "" { + attrs = append(attrs, otelkey.String("net.host.ip", hostIP)) + } + if hostName != "" { + attrs = append(attrs, otelkey.String("net.host.name", hostName)) + } + if hostPort != 0 { + attrs = append(attrs, otelkey.Int("net.host.port", hostPort)) + } + return attrs +} + +// EndUserAttributesFromHTTPRequest generates attributes of the +// enduser namespace as specified by the OpenTelemetry specification +// for a span. +func EndUserAttributesFromHTTPRequest(request *http.Request) []otelcore.KeyValue { + if username, _, ok := request.BasicAuth(); ok { + return []otelcore.KeyValue{otelkey.String("enduser.id", username)} + } + return nil +} + +// HTTPServerAttributesFromHTTPRequest generates attributes of the +// http namespace as specified by the OpenTelemetry specification for +// a span on the server side. Currently, only basic authentication is +// supported. +func HTTPServerAttributesFromHTTPRequest(serverName, route string, request *http.Request) []otelcore.KeyValue { + attrs := []otelcore.KeyValue{ + otelkey.String("http.method", request.Method), + otelkey.String("http.target", request.RequestURI), + } + if serverName != "" { + attrs = append(attrs, otelkey.String("http.server_name", serverName)) + } + scheme := "" + if request.TLS != nil { + scheme = "https" + } else { + scheme = "http" + } + attrs = append(attrs, otelkey.String("http.scheme", scheme)) + if route != "" { + attrs = append(attrs, otelkey.String("http.route", route)) + } + if request.Host != "" { + attrs = append(attrs, otelkey.String("http.host", request.Host)) + } + if ua := request.UserAgent(); ua != "" { + attrs = append(attrs, otelkey.String("http.user_agent", ua)) + } + if values, ok := request.Header["X-Forwarded-For"]; ok && len(values) > 0 { + attrs = append(attrs, otelkey.String("http.client_ip", values[0])) + } + flavor := "" + if request.ProtoMajor == 1 { + flavor = fmt.Sprintf("1.%d", request.ProtoMinor) + } else if request.ProtoMajor == 2 { + flavor = "2" + } + if flavor != "" { + attrs = append(attrs, otelkey.String("http.flavor", flavor)) + } + return attrs +} + +// HTTPAttributesFromHTTPStatusCode generates attributes of the http +// namespace as specified by the OpenTelemetry specification for a +// span. +func HTTPAttributesFromHTTPStatusCode(code int) []otelcore.KeyValue { + attrs := []otelcore.KeyValue{ + otelkey.Int("http.status_code", code), + } + text := http.StatusText(code) + if text != "" { + attrs = append(attrs, otelkey.String("http.status_text", text)) + } + return attrs +} + +type codeRange struct { + fromInclusive int + toInclusive int +} + +func (r codeRange) contains(code int) bool { + return r.fromInclusive <= code && code <= r.toInclusive +} + +var validRangesPerCategory = map[int][]codeRange{ + 1: { + {http.StatusContinue, http.StatusEarlyHints}, + }, + 2: { + {http.StatusOK, http.StatusAlreadyReported}, + {http.StatusIMUsed, http.StatusIMUsed}, + }, + 3: { + {http.StatusMultipleChoices, http.StatusUseProxy}, + {http.StatusTemporaryRedirect, http.StatusPermanentRedirect}, + }, + 4: { + {http.StatusBadRequest, http.StatusTeapot}, // yes, teapot is so useful… + {http.StatusMisdirectedRequest, http.StatusUpgradeRequired}, + {http.StatusPreconditionRequired, http.StatusTooManyRequests}, + {http.StatusRequestHeaderFieldsTooLarge, http.StatusRequestHeaderFieldsTooLarge}, + {http.StatusUnavailableForLegalReasons, http.StatusUnavailableForLegalReasons}, + }, + 5: { + {http.StatusInternalServerError, http.StatusLoopDetected}, + {http.StatusNotExtended, http.StatusNetworkAuthenticationRequired}, + }, +} + +// SpanStatusFromHTTPStatusCode generates a status code and a message +// as specified by the OpenTelemetry specification for a span. +func SpanStatusFromHTTPStatusCode(code int) (codes.Code, string) { + spanCode := func() codes.Code { + category := code / 100 + ranges, ok := validRangesPerCategory[category] + if !ok { + return codes.Unknown + } + ok = false + for _, crange := range ranges { + ok = crange.contains(code) + if ok { + break + } + } + if !ok { + return codes.Unknown + } + switch code { + case http.StatusUnauthorized: + return codes.Unauthenticated + case http.StatusForbidden: + return codes.PermissionDenied + case http.StatusNotFound: + return codes.NotFound + case http.StatusTooManyRequests: + return codes.ResourceExhausted + case http.StatusNotImplemented: + return codes.Unimplemented + case http.StatusServiceUnavailable: + return codes.Unavailable + case http.StatusGatewayTimeout: + return codes.DeadlineExceeded + } + if category > 0 && category < 4 { + return codes.OK + } + if category == 4 { + return codes.InvalidArgument + } + if category == 5 { + return codes.Internal + } + // this really should not happen, if we get there then + // it means that the code got out of sync with + // validRangesPerCategory map + return codes.Unknown + }() + if spanCode == codes.Unknown { + return spanCode, fmt.Sprintf("Invalid HTTP status code %d", code) + } + return spanCode, fmt.Sprintf("HTTP status code: %d", code) +} diff --git a/internal/trace/http_test.go b/internal/trace/http_test.go new file mode 100644 index 00000000000..61bf47b5703 --- /dev/null +++ b/internal/trace/http_test.go @@ -0,0 +1,778 @@ +// 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 trace + +import ( + "crypto/tls" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + + otelcore "go.opentelemetry.io/otel/api/core" + otelkey "go.opentelemetry.io/otel/api/key" +) + +type tlsOption int + +const ( + noTLS tlsOption = iota + withTLS +) + +func TestNetAttributesFromHTTPRequest(t *testing.T) { + type testcase struct { + name string + + network string + + method string + requestURI string + proto string + remoteAddr string + host string + url *url.URL + header http.Header + + expected []otelcore.KeyValue + } + testcases := []testcase{ + { + name: "stripped, tcp", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + }, + }, + { + name: "stripped, udp", + network: "udp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.UDP"), + }, + }, + { + name: "stripped, ip", + network: "ip", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP"), + }, + }, + { + name: "stripped, unix", + network: "unix", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "Unix"), + }, + }, + { + name: "stripped, other", + network: "nih", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "other"), + }, + }, + { + name: "with remote ip and port", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + }, + }, + { + name: "with remote name and port", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "example.com:56", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.name", "example.com"), + otelkey.Int("net.peer.port", 56), + }, + }, + { + name: "with remote ip only", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + }, + }, + { + name: "with remote name only", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "example.com", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.name", "example.com"), + }, + }, + { + name: "with remote port only", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: ":56", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + }, + }, + { + name: "with host name only", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "example.com", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.name", "example.com"), + }, + }, + { + name: "with host ip only", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "4.3.2.1", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.ip", "4.3.2.1"), + }, + }, + { + name: "with host name and port", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "example.com:78", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.name", "example.com"), + otelkey.Int("net.host.port", 78), + }, + }, + { + name: "with host ip and port", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "4.3.2.1:78", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.ip", "4.3.2.1"), + otelkey.Int("net.host.port", 78), + }, + }, + { + name: "with host name and bogus port", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "example.com:qwerty", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.name", "example.com"), + }, + }, + { + name: "with host ip and bogus port", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "4.3.2.1:qwerty", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.ip", "4.3.2.1"), + }, + }, + { + name: "with empty host and port", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: ":80", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + }, + }, + { + name: "with host ip and port in headers", + network: "tcp", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: http.Header{ + "Host": []string{"4.3.2.1:78"}, + }, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.ip", "4.3.2.1"), + otelkey.Int("net.host.port", 78), + }, + }, + { + name: "with host ip and port in url", + network: "tcp", + method: "GET", + requestURI: "http://4.3.2.1:78/user/123", + proto: "HTTP/1.0", + remoteAddr: "1.2.3.4:56", + host: "", + url: &url.URL{ + Host: "4.3.2.1:78", + Path: "/user/123", + }, + header: nil, + expected: []otelcore.KeyValue{ + otelkey.String("net.transport", "IP.TCP"), + otelkey.String("net.peer.ip", "1.2.3.4"), + otelkey.Int("net.peer.port", 56), + otelkey.String("net.host.ip", "4.3.2.1"), + otelkey.Int("net.host.port", 78), + }, + }, + } + for idx, tc := range testcases { + r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, noTLS) + got := NetAttributesFromHTTPRequest(tc.network, r) + assertElementsMatch(t, tc.expected, got, "testcase %d - %s", idx, tc.name) + } +} + +func TestEndUserAttributesFromHTTPRequest(t *testing.T) { + r := testRequest("GET", "/user/123", "HTTP/1.1", "", "", nil, http.Header{}, withTLS) + var expected []otelcore.KeyValue + got := EndUserAttributesFromHTTPRequest(r) + assert.ElementsMatch(t, expected, got) + r.SetBasicAuth("admin", "password") + expected = []otelcore.KeyValue{otelkey.String("enduser.id", "admin")} + got = EndUserAttributesFromHTTPRequest(r) + assert.ElementsMatch(t, expected, got) +} + +func TestHTTPServerAttributesFromHTTPRequest(t *testing.T) { + type testcase struct { + name string + + serverName string + route string + + method string + requestURI string + proto string + remoteAddr string + host string + url *url.URL + header http.Header + tls tlsOption + + expected []otelcore.KeyValue + } + testcases := []testcase{ + { + name: "stripped", + serverName: "", + route: "", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + tls: noTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "http"), + otelkey.String("http.flavor", "1.0"), + }, + }, + { + name: "with server name", + serverName: "my-server-name", + route: "", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + tls: noTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "http"), + otelkey.String("http.flavor", "1.0"), + otelkey.String("http.server_name", "my-server-name"), + }, + }, + { + name: "with tls", + serverName: "my-server-name", + route: "", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + tls: withTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "https"), + otelkey.String("http.flavor", "1.0"), + otelkey.String("http.server_name", "my-server-name"), + }, + }, + { + name: "with route", + serverName: "my-server-name", + route: "/user/:id", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + tls: withTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "https"), + otelkey.String("http.flavor", "1.0"), + otelkey.String("http.server_name", "my-server-name"), + otelkey.String("http.route", "/user/:id"), + }, + }, + { + name: "with host", + serverName: "my-server-name", + route: "/user/:id", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "example.com", + url: &url.URL{ + Path: "/user/123", + }, + header: nil, + tls: withTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "https"), + otelkey.String("http.flavor", "1.0"), + otelkey.String("http.server_name", "my-server-name"), + otelkey.String("http.route", "/user/:id"), + otelkey.String("http.host", "example.com"), + }, + }, + { + name: "with user agent", + serverName: "my-server-name", + route: "/user/:id", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "example.com", + url: &url.URL{ + Path: "/user/123", + }, + header: http.Header{ + "User-Agent": []string{"foodownloader"}, + }, + tls: withTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "https"), + otelkey.String("http.flavor", "1.0"), + otelkey.String("http.server_name", "my-server-name"), + otelkey.String("http.route", "/user/:id"), + otelkey.String("http.host", "example.com"), + otelkey.String("http.user_agent", "foodownloader"), + }, + }, + { + name: "with proxy info", + serverName: "my-server-name", + route: "/user/:id", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.0", + remoteAddr: "", + host: "example.com", + url: &url.URL{ + Path: "/user/123", + }, + header: http.Header{ + "User-Agent": []string{"foodownloader"}, + "X-Forwarded-For": []string{"1.2.3.4"}, + }, + tls: withTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "https"), + otelkey.String("http.flavor", "1.0"), + otelkey.String("http.server_name", "my-server-name"), + otelkey.String("http.route", "/user/:id"), + otelkey.String("http.host", "example.com"), + otelkey.String("http.user_agent", "foodownloader"), + otelkey.String("http.client_ip", "1.2.3.4"), + }, + }, + { + name: "with http 1.1", + serverName: "my-server-name", + route: "/user/:id", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/1.1", + remoteAddr: "", + host: "example.com", + url: &url.URL{ + Path: "/user/123", + }, + header: http.Header{ + "User-Agent": []string{"foodownloader"}, + "X-Forwarded-For": []string{"1.2.3.4"}, + }, + tls: withTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "https"), + otelkey.String("http.flavor", "1.1"), + otelkey.String("http.server_name", "my-server-name"), + otelkey.String("http.route", "/user/:id"), + otelkey.String("http.host", "example.com"), + otelkey.String("http.user_agent", "foodownloader"), + otelkey.String("http.client_ip", "1.2.3.4"), + }, + }, + { + name: "with http 2", + serverName: "my-server-name", + route: "/user/:id", + method: "GET", + requestURI: "/user/123", + proto: "HTTP/2.0", + remoteAddr: "", + host: "example.com", + url: &url.URL{ + Path: "/user/123", + }, + header: http.Header{ + "User-Agent": []string{"foodownloader"}, + "X-Forwarded-For": []string{"1.2.3.4"}, + }, + tls: withTLS, + expected: []otelcore.KeyValue{ + otelkey.String("http.method", "GET"), + otelkey.String("http.target", "/user/123"), + otelkey.String("http.scheme", "https"), + otelkey.String("http.flavor", "2"), + otelkey.String("http.server_name", "my-server-name"), + otelkey.String("http.route", "/user/:id"), + otelkey.String("http.host", "example.com"), + otelkey.String("http.user_agent", "foodownloader"), + otelkey.String("http.client_ip", "1.2.3.4"), + }, + }, + } + for idx, tc := range testcases { + r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, tc.tls) + got := HTTPServerAttributesFromHTTPRequest(tc.serverName, tc.route, r) + assertElementsMatch(t, tc.expected, got, "testcase %d - %s", idx, tc.name) + } +} + +func TestHTTPAttributesFromHTTPStatusCode(t *testing.T) { + expected := []otelcore.KeyValue{ + otelkey.Int("http.status_code", 404), + otelkey.String("http.status_text", "Not Found"), + } + got := HTTPAttributesFromHTTPStatusCode(http.StatusNotFound) + assertElementsMatch(t, expected, got, "with valid HTTP status code") + assert.ElementsMatch(t, expected, got) + expected = []otelcore.KeyValue{ + otelkey.Int("http.status_code", 499), + } + got = HTTPAttributesFromHTTPStatusCode(499) + assertElementsMatch(t, expected, got, "with invalid HTTP status code") +} + +func TestSpanStatusFromHTTPStatusCode(t *testing.T) { + for code := 0; code < 1000; code++ { + expected := getExpectedGRPCCodeForHTTPCode(code) + got, _ := SpanStatusFromHTTPStatusCode(code) + assert.Equalf(t, expected, got, "%s vs %s", expected, got) + } +} + +func getExpectedGRPCCodeForHTTPCode(code int) codes.Code { + if http.StatusText(code) == "" { + return codes.Unknown + } + switch code { + case http.StatusUnauthorized: + return codes.Unauthenticated + case http.StatusForbidden: + return codes.PermissionDenied + case http.StatusNotFound: + return codes.NotFound + case http.StatusTooManyRequests: + return codes.ResourceExhausted + case http.StatusNotImplemented: + return codes.Unimplemented + case http.StatusServiceUnavailable: + return codes.Unavailable + case http.StatusGatewayTimeout: + return codes.DeadlineExceeded + } + category := code / 100 + if category < 4 { + return codes.OK + } + if category < 5 { + return codes.InvalidArgument + } + return codes.Internal +} + +func assertElementsMatch(t *testing.T, expected, got []otelcore.KeyValue, format string, args ...interface{}) { + if !assert.ElementsMatchf(t, expected, got, format, args...) { + t.Log("expected:", kvStr(expected)) + t.Log("got:", kvStr(got)) + } +} + +func testRequest(method, requestURI, proto, remoteAddr, host string, u *url.URL, header http.Header, tlsopt tlsOption) *http.Request { + major, minor := protoToInts(proto) + var tlsConn *tls.ConnectionState + switch tlsopt { + case noTLS: + case withTLS: + tlsConn = &tls.ConnectionState{} + } + return &http.Request{ + Method: method, + URL: u, + Proto: proto, + ProtoMajor: major, + ProtoMinor: minor, + Header: header, + Host: host, + RemoteAddr: remoteAddr, + RequestURI: requestURI, + TLS: tlsConn, + } +} + +func protoToInts(proto string) (int, int) { + switch proto { + case "HTTP/1.0": + return 1, 0 + case "HTTP/1.1": + return 1, 1 + case "HTTP/2.0": + return 2, 0 + } + // invalid proto + return 13, 42 +} + +func kvStr(kvs []otelcore.KeyValue) string { + sb := strings.Builder{} + sb.WriteRune('[') + for idx, kv := range kvs { + if idx > 0 { + sb.WriteString(", ") + } + sb.WriteString((string)(kv.Key)) + sb.WriteString(": ") + sb.WriteString(kv.Value.Emit()) + } + sb.WriteRune(']') + return sb.String() +} From 78440c7f11d4e41e8cb91ced1f69c8f1bd3543ce Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 22 Apr 2020 20:20:39 +0200 Subject: [PATCH 3/4] Add gorilla mux instrumentation --- plugins/gorilla/mux/config.go | 48 +++++++++ plugins/gorilla/mux/doc.go | 20 ++++ plugins/gorilla/mux/go.mod | 12 +++ plugins/gorilla/mux/go.sum | 87 +++++++++++++++ plugins/gorilla/mux/mux.go | 147 +++++++++++++++++++++++++ plugins/gorilla/mux/mux_test.go | 183 ++++++++++++++++++++++++++++++++ 6 files changed, 497 insertions(+) create mode 100644 plugins/gorilla/mux/config.go create mode 100644 plugins/gorilla/mux/doc.go create mode 100644 plugins/gorilla/mux/go.mod create mode 100644 plugins/gorilla/mux/go.sum create mode 100644 plugins/gorilla/mux/mux.go create mode 100644 plugins/gorilla/mux/mux_test.go diff --git a/plugins/gorilla/mux/config.go b/plugins/gorilla/mux/config.go new file mode 100644 index 00000000000..eaeac29fadc --- /dev/null +++ b/plugins/gorilla/mux/config.go @@ -0,0 +1,48 @@ +// 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 mux + +import ( + otelpropagation "go.opentelemetry.io/otel/api/propagation" + oteltrace "go.opentelemetry.io/otel/api/trace" +) + +// Config is used to configure the mux middleware. +type Config struct { + Tracer oteltrace.Tracer + Propagators otelpropagation.Propagators +} + +// Option specifies instrumentation configuration options. +type Option func(*Config) + +// WithTracer specifies a tracer to use for creating spans. If none is +// specified, a tracer named +// "go.opentelemetry.io/contrib/plugins/gorilla/mux" from the global +// provider is used. +func WithTracer(tracer oteltrace.Tracer) Option { + return func(cfg *Config) { + cfg.Tracer = tracer + } +} + +// WithPropagators specifies propagators to use for extracting +// information from the HTTP requests. If none are specified, global +// ones will be used. +func WithPropagators(propagators otelpropagation.Propagators) Option { + return func(cfg *Config) { + cfg.Propagators = propagators + } +} diff --git a/plugins/gorilla/mux/doc.go b/plugins/gorilla/mux/doc.go new file mode 100644 index 00000000000..4a1ba73b60f --- /dev/null +++ b/plugins/gorilla/mux/doc.go @@ -0,0 +1,20 @@ +// 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 mux provides functions to trace the gorilla/mux package +// (https://github.com/gorilla/mux). +// +// Currently only the routing of a received message can be +// instrumented. To do it, use the Middleware function. +package mux // import "go.opentelemetry.io/contrib/plugins/gorilla/mux" diff --git a/plugins/gorilla/mux/go.mod b/plugins/gorilla/mux/go.mod new file mode 100644 index 00000000000..752a5a5be49 --- /dev/null +++ b/plugins/gorilla/mux/go.mod @@ -0,0 +1,12 @@ +module go.opentelemetry.io/contrib/plugins/gorilla/mux + +go 1.14 + +replace go.opentelemetry.io/contrib => ../../.. + +require ( + github.com/gorilla/mux v1.7.4 + github.com/stretchr/testify v1.4.0 + go.opentelemetry.io/contrib v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/otel v0.4.2 +) diff --git a/plugins/gorilla/mux/go.sum b/plugins/gorilla/mux/go.sum new file mode 100644 index 00000000000..1abba5efbcd --- /dev/null +++ b/plugins/gorilla/mux/go.sum @@ -0,0 +1,87 @@ +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/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs= +github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= +github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w= +github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +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/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.opentelemetry.io/otel v0.4.2 h1:nT+GOqqRR1cIY92xmo1DeiXLHtIlXH1KLRgnsnhuNrs= +go.opentelemetry.io/otel v0.4.2/go.mod h1:OgNpQOjrlt33Ew6Ds0mGjmcTQg/rhUctsbkRdk/g1fw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +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-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +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.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.1 h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k= +google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= diff --git a/plugins/gorilla/mux/mux.go b/plugins/gorilla/mux/mux.go new file mode 100644 index 00000000000..b96ba44238e --- /dev/null +++ b/plugins/gorilla/mux/mux.go @@ -0,0 +1,147 @@ +// 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 mux + +import ( + "fmt" + "net/http" + "sync" + + "github.com/gorilla/mux" + + "go.opentelemetry.io/contrib/internal/trace" + otelglobal "go.opentelemetry.io/otel/api/global" + otelpropagation "go.opentelemetry.io/otel/api/propagation" + oteltrace "go.opentelemetry.io/otel/api/trace" +) + +const ( + tracerName = "go.opentelemetry.io/contrib/plugins/gorilla/mux" +) + +// Middleware sets up a handler to start tracing the incoming +// requests. The service parameter should describe the name of the +// (virtual) server handling the request. +func Middleware(service string, opts ...Option) mux.MiddlewareFunc { + cfg := Config{} + for _, opt := range opts { + opt(&cfg) + } + if cfg.Tracer == nil { + cfg.Tracer = otelglobal.Tracer(tracerName) + } + if cfg.Propagators == nil { + cfg.Propagators = otelglobal.Propagators() + } + return func(handler http.Handler) http.Handler { + return traceware{ + service: service, + tracer: cfg.Tracer, + propagators: cfg.Propagators, + handler: handler, + } + } +} + +type traceware struct { + service string + tracer oteltrace.Tracer + propagators otelpropagation.Propagators + handler http.Handler +} + +type recordingResponseWriter struct { + writer http.ResponseWriter + written bool + status int +} + +func (w *recordingResponseWriter) Header() http.Header { + return w.writer.Header() +} + +func (w *recordingResponseWriter) Write(slice []byte) (int, error) { + w.writeHeader(http.StatusOK) + return w.writer.Write(slice) +} + +func (w *recordingResponseWriter) WriteHeader(statusCode int) { + w.writeHeader(statusCode) + w.writer.WriteHeader(statusCode) +} + +func (w *recordingResponseWriter) writeHeader(statusCode int) { + if !w.written { + w.written = true + w.status = statusCode + } +} + +var rrwPool = &sync.Pool{ + New: func() interface{} { + return &recordingResponseWriter{} + }, +} + +func getRRW(writer http.ResponseWriter) *recordingResponseWriter { + rrw := rrwPool.Get().(*recordingResponseWriter) + rrw.written = false + rrw.status = 0 + rrw.writer = writer + return rrw +} + +func putRRW(rrw *recordingResponseWriter) { + rrw.writer = nil + rrwPool.Put(rrw) +} + +// ServeHTTP implements the http.Handler interface. It does the actual +// tracing of the request. +func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := otelpropagation.ExtractHTTP(r.Context(), tw.propagators, r.Header) + spanName := "" + route := mux.CurrentRoute(r) + if route != nil { + var err error + spanName, err = route.GetPathTemplate() + if err != nil { + spanName, err = route.GetPathRegexp() + if err != nil { + spanName = "" + } + } + } + routeStr := spanName + if spanName == "" { + spanName = fmt.Sprintf("HTTP %s route not found", r.Method) + } + opts := []oteltrace.StartOption{ + oteltrace.WithAttributes(trace.NetAttributesFromHTTPRequest("tcp", r)...), + oteltrace.WithAttributes(trace.EndUserAttributesFromHTTPRequest(r)...), + oteltrace.WithAttributes(trace.HTTPServerAttributesFromHTTPRequest(tw.service, routeStr, r)...), + oteltrace.WithSpanKind(oteltrace.SpanKindServer), + } + ctx, span := tw.tracer.Start(ctx, spanName, opts...) + defer span.End() + r2 := r.WithContext(ctx) + rrw := getRRW(w) + defer putRRW(rrw) + tw.handler.ServeHTTP(rrw, r2) + attrs := trace.HTTPAttributesFromHTTPStatusCode(rrw.status) + spanStatus, spanMessage := trace.SpanStatusFromHTTPStatusCode(rrw.status) + span.SetAttributes(attrs...) + span.SetStatus(spanStatus, spanMessage) +} diff --git a/plugins/gorilla/mux/mux_test.go b/plugins/gorilla/mux/mux_test.go new file mode 100644 index 00000000000..dfecea08006 --- /dev/null +++ b/plugins/gorilla/mux/mux_test.go @@ -0,0 +1,183 @@ +// 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 mux + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + mocktrace "go.opentelemetry.io/contrib/internal/trace" + otelcore "go.opentelemetry.io/otel/api/core" + otelglobal "go.opentelemetry.io/otel/api/global" + otelpropagation "go.opentelemetry.io/otel/api/propagation" + oteltrace "go.opentelemetry.io/otel/api/trace" +) + +func TestChildSpanFromGlobalTracer(t *testing.T) { + otelglobal.SetTraceProvider(&mocktrace.Provider{}) + + router := mux.NewRouter() + router.Use(Middleware("foobar")) + router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := oteltrace.SpanFromContext(r.Context()) + _, ok := span.(*mocktrace.Span) + assert.True(t, ok) + spanTracer := span.Tracer() + mockTracer, ok := spanTracer.(*mocktrace.Tracer) + require.True(t, ok) + assert.Equal(t, "go.opentelemetry.io/contrib/plugins/gorilla/mux", mockTracer.Name) + w.WriteHeader(http.StatusOK) + })) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, r) +} + +func TestChildSpanFromCustomTracer(t *testing.T) { + tracer := mocktrace.NewTracer("test-tracer") + + router := mux.NewRouter() + router.Use(Middleware("foobar", WithTracer(tracer))) + router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := oteltrace.SpanFromContext(r.Context()) + _, ok := span.(*mocktrace.Span) + assert.True(t, ok) + spanTracer := span.Tracer() + mockTracer, ok := spanTracer.(*mocktrace.Tracer) + require.True(t, ok) + assert.Equal(t, "test-tracer", mockTracer.Name) + w.WriteHeader(http.StatusOK) + })) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, r) +} + +func TestChildSpanNames(t *testing.T) { + tracer := mocktrace.NewTracer("test-tracer") + + router := mux.NewRouter() + router.Use(Middleware("foobar", WithTracer(tracer))) + router.HandleFunc("/user/{id:[0-9]+}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + router.HandleFunc("/book/{title}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(([]byte)("ok")) + })) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + spans := tracer.EndedSpans() + require.Len(t, spans, 1) + span := spans[0] + assert.Equal(t, "/user/{id:[0-9]+}", span.Name) + assert.Equal(t, oteltrace.SpanKindServer, span.Kind) + assert.Equal(t, otelcore.String("foobar"), span.Attributes["http.server_name"]) + assert.Equal(t, otelcore.Int(http.StatusOK), span.Attributes["http.status_code"]) + assert.Equal(t, otelcore.String("GET"), span.Attributes["http.method"]) + assert.Equal(t, otelcore.String("/user/123"), span.Attributes["http.target"]) + assert.Equal(t, otelcore.String("/user/{id:[0-9]+}"), span.Attributes["http.route"]) + + r = httptest.NewRequest("GET", "/book/foo", nil) + w = httptest.NewRecorder() + router.ServeHTTP(w, r) + spans = tracer.EndedSpans() + require.Len(t, spans, 1) + span = spans[0] + assert.Equal(t, "/book/{title}", span.Name) + assert.Equal(t, oteltrace.SpanKindServer, span.Kind) + assert.Equal(t, otelcore.String("foobar"), span.Attributes["http.server_name"]) + assert.Equal(t, otelcore.Int(http.StatusOK), span.Attributes["http.status_code"]) + assert.Equal(t, otelcore.String("GET"), span.Attributes["http.method"]) + assert.Equal(t, otelcore.String("/book/foo"), span.Attributes["http.target"]) + assert.Equal(t, otelcore.String("/book/{title}"), span.Attributes["http.route"]) +} + +func TestGetSpanNotInstrumented(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := oteltrace.SpanFromContext(r.Context()) + _, ok := span.(oteltrace.NoopSpan) + assert.True(t, ok) + w.WriteHeader(http.StatusOK) + })) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, r) +} + +func TestPropagationWithGlobalPropagators(t *testing.T) { + tracer := mocktrace.NewTracer("test-tracer") + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + ctx, pspan := tracer.Start(context.Background(), "test") + otelpropagation.InjectHTTP(ctx, otelglobal.Propagators(), r.Header) + + router := mux.NewRouter() + router.Use(Middleware("foobar", WithTracer(tracer))) + router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := oteltrace.SpanFromContext(r.Context()) + mspan, ok := span.(*mocktrace.Span) + require.True(t, ok) + assert.Equal(t, pspan.SpanContext().TraceID, mspan.SpanContext().TraceID) + assert.Equal(t, pspan.SpanContext().SpanID, mspan.ParentSpanID) + w.WriteHeader(http.StatusOK) + })) + + router.ServeHTTP(w, r) +} + +func TestPropagationWithCustomPropagators(t *testing.T) { + tracer := mocktrace.NewTracer("test-tracer") + b3 := oteltrace.B3{} + props := otelpropagation.New( + otelpropagation.WithExtractors(b3), + otelpropagation.WithInjectors(b3), + ) + + r := httptest.NewRequest("GET", "/user/123", nil) + w := httptest.NewRecorder() + + ctx, pspan := tracer.Start(context.Background(), "test") + otelpropagation.InjectHTTP(ctx, props, r.Header) + + router := mux.NewRouter() + router.Use(Middleware("foobar", WithTracer(tracer), WithPropagators(props))) + router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := oteltrace.SpanFromContext(r.Context()) + mspan, ok := span.(*mocktrace.Span) + require.True(t, ok) + assert.Equal(t, pspan.SpanContext().TraceID, mspan.SpanContext().TraceID) + assert.Equal(t, pspan.SpanContext().SpanID, mspan.ParentSpanID) + w.WriteHeader(http.StatusOK) + })) + + router.ServeHTTP(w, r) +} From 9b73756566fc3d4da1b062105a237b177c1d2b4d Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 22 Apr 2020 22:15:34 +0200 Subject: [PATCH 4/4] Add example for gorilla mux instrumentation --- plugins/gorilla/mux/example/Dockerfile | 20 +++++ plugins/gorilla/mux/example/README.md | 28 +++++++ .../gorilla/mux/example/docker-compose.yml | 39 ++++++++++ plugins/gorilla/mux/example/server.go | 75 +++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 plugins/gorilla/mux/example/Dockerfile create mode 100644 plugins/gorilla/mux/example/README.md create mode 100644 plugins/gorilla/mux/example/docker-compose.yml create mode 100644 plugins/gorilla/mux/example/server.go diff --git a/plugins/gorilla/mux/example/Dockerfile b/plugins/gorilla/mux/example/Dockerfile new file mode 100644 index 00000000000..23a8e9b5465 --- /dev/null +++ b/plugins/gorilla/mux/example/Dockerfile @@ -0,0 +1,20 @@ +# 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. +FROM golang:alpine AS base +COPY . /src/ +WORKDIR /src/plugins/gorilla/mux + +FROM base AS mux-server +RUN go install ./example/server.go +CMD ["/go/bin/server"] diff --git a/plugins/gorilla/mux/example/README.md b/plugins/gorilla/mux/example/README.md new file mode 100644 index 00000000000..3b201c743ee --- /dev/null +++ b/plugins/gorilla/mux/example/README.md @@ -0,0 +1,28 @@ +# gorilla/mux instrumentation example + +An HTTP server using gorilla/mux and instrumentation. The server has a +`/users/{id:[0-9]+}` endpoint. The server generates span information to +`stdout`. + +These instructions expect you have +[docker-compose](https://docs.docker.com/compose/) installed. + +Bring up the `mux-server` and `mux-client` services to run the +example: + +```sh +docker-compose up --detach mux-server mux-client +``` + +The `mux-client` service sends just one HTTP request to `mux-server` +and then exits. View the span generated by `mux-server` in the logs: + +```sh +docker-compose logs mux-server +``` + +Shut down the services when you are finished with the example: + +```sh +docker-compose down +``` diff --git a/plugins/gorilla/mux/example/docker-compose.yml b/plugins/gorilla/mux/example/docker-compose.yml new file mode 100644 index 00000000000..eb7b2c768ce --- /dev/null +++ b/plugins/gorilla/mux/example/docker-compose.yml @@ -0,0 +1,39 @@ +# 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. +version: "3.7" +services: + mux-client: + image: golang:alpine + networks: + - example + command: + - "/bin/sh" + - "-c" + - "wget http://mux-server:8080/users/123 && cat 123" + depends_on: + - mux-server + mux-server: + build: + dockerfile: $PWD/Dockerfile + context: ../../../.. + ports: + - "8080:80" + command: + - "/bin/sh" + - "-c" + - "/go/bin/server" + networks: + - example +networks: + example: diff --git a/plugins/gorilla/mux/example/server.go b/plugins/gorilla/mux/example/server.go new file mode 100644 index 00000000000..c7162a36c74 --- /dev/null +++ b/plugins/gorilla/mux/example/server.go @@ -0,0 +1,75 @@ +// 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 main + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + + muxtrace "go.opentelemetry.io/contrib/plugins/gorilla/mux" + otelglobal "go.opentelemetry.io/otel/api/global" + otelkey "go.opentelemetry.io/otel/api/key" + oteltrace "go.opentelemetry.io/otel/api/trace" + oteltracestdout "go.opentelemetry.io/otel/exporters/trace/stdout" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +var tracer = otelglobal.Tracer("mux-server") + +func main() { + initTracer() + r := mux.NewRouter() + r.Use(muxtrace.Middleware("my-server")) + r.HandleFunc("/users/{id:[0-9]+}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + name := getUser(r.Context(), id) + reply := fmt.Sprintf("user %s (id %s)\n", name, id) + _, _ = w.Write(([]byte)(reply)) + })) + http.Handle("/", r) + _ = http.ListenAndServe(":8080", nil) +} + +func initTracer() { + exporter, err := oteltracestdout.NewExporter(oteltracestdout.Options{PrettyPrint: true}) + if err != nil { + log.Fatal(err) + } + cfg := sdktrace.Config{ + DefaultSampler: sdktrace.AlwaysSample(), + } + tp, err := sdktrace.NewProvider( + sdktrace.WithConfig(cfg), + sdktrace.WithSyncer(exporter), + ) + if err != nil { + log.Fatal(err) + } + otelglobal.SetTraceProvider(tp) +} + +func getUser(ctx context.Context, id string) string { + _, span := tracer.Start(ctx, "getUser", oteltrace.WithAttributes(otelkey.String("id", id))) + defer span.End() + if id == "123" { + return "muxtrace tester" + } + return "unknown" +}