diff --git a/example/README.md b/example/README.md new file mode 100644 index 000000000..a7538d015 --- /dev/null +++ b/example/README.md @@ -0,0 +1,59 @@ +# Example + +## HTTP +This is a simple example that demonstrates tracing http request from client to server. The example +shows key aspects of tracing such as +- Root Span (on Client) +- Child Span (on Client) +- Child Span from a Remote Parent (on Server) +- SpanContext Propagation (from Client to Server) +- Span Events +- Span Attributes + +Example uses +- open-telemetry SDK as trace instrumentation provider, +- httptrace plugin to facilitate tracing http request on client and server +- http trace_context propagation to propagate SpanContext on the wire. +- jaeger exporter to export spans to visualize and store them. + +### How to run? + +#### Prequisites + +- go 1.12 installed +- GOPATH is configured. + +#### 1 Download git repo +``` +GO111MODULE="" go get -d go.opentelemetry.io +``` + +#### 2 Start All-in-one Jaeger + +``` +docker run -d --name jaeger \ + -p 16686:16686 \ + -p 14268:14268 \ + jaegertracing/all-in-one:1.8 +``` + +#### 3 Start Server +``` +cd $GOPATH/src/go.opentelemetry.io/example/http/ +go run ./server/server.go +``` + +#### 4 Start Client +``` +cd $GOPATH/src/go.opentelemetry.io/example/http/ +go run ./client/client.go +``` + +#### 5 Check traces on Jaeger UI + +Visit http://localhost:16686 with a web browser +Click on 'Find' to see traces. + +[Sample Snapshot](http/images/JaegarTraceExample.png) + + diff --git a/example/http/client/client.go b/example/http/client/client.go index bdd2520fb..8b8a99176 100644 --- a/example/http/client/client.go +++ b/example/http/client/client.go @@ -18,27 +18,49 @@ import ( "context" "fmt" "io/ioutil" + "log" + "net/http" + "time" "google.golang.org/grpc/codes" "go.opentelemetry.io/api/key" "go.opentelemetry.io/api/tag" "go.opentelemetry.io/api/trace" + "go.opentelemetry.io/exporter/trace/jaeger" "go.opentelemetry.io/plugin/httptrace" + sdktrace "go.opentelemetry.io/sdk/trace" ) -var ( - tracer = trace.GlobalTracer(). - WithService("client"). - WithComponent("main"). - WithResources( - key.New("whatevs").String("yesss"), - ) -) +func initTracer() { + // Register SDK as trace provider. + sdktrace.Register() + + // Create Jaeger exporter to be able to retrieve + // the collected spans. + exporter, err := jaeger.NewExporter(jaeger.Options{ + CollectorEndpoint: "http://localhost:14268/api/traces", + Process: jaeger.Process{ + ServiceName: "trace-demo", + }, + }) + if err != nil { + log.Fatal(err) + } + + // Wrap Jaeger exporter with SimpleSpanProcessor and register the processor. + ssp := sdktrace.NewSimpleSpanProcessor(exporter) + sdktrace.RegisterSpanProcessor(ssp) + + // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. + // In a production application, use sdktrace.ProbabilitySampler with a desired probability. + sdktrace.ApplyConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}) +} func main() { - fmt.Printf("Tracer %v\n", tracer) + initTracer() + client := http.DefaultClient ctx := tag.NewContext(context.Background(), tag.Insert(key.New("username").String("donuts")), @@ -46,13 +68,14 @@ func main() { var body []byte - err := tracer.WithSpan(ctx, "say hello", + err := trace.GlobalTracer().WithSpan(ctx, "say hello", func(ctx context.Context) error { req, _ := http.NewRequest("GET", "http://localhost:7777/hello", nil) ctx, req = httptrace.W3C(ctx, req) httptrace.Inject(ctx, req) + fmt.Printf("Sending request...\n") res, err := client.Do(req) if err != nil { panic(err) @@ -68,5 +91,8 @@ func main() { panic(err) } - fmt.Printf("%s", body) + fmt.Printf("Response Received: %s\n\n\n", body) + fmt.Printf("Waiting for few seconds to export spans ...\n\n") + time.Sleep(10 * time.Second) + fmt.Printf("Check traces on http://localhost:16686\n") } diff --git a/example/http/go.sum b/example/http/go.sum index 4e7e90a93..41694af5f 100644 --- a/example/http/go.sum +++ b/example/http/go.sum @@ -4,6 +4,7 @@ cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSR github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -64,6 +65,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -188,6 +190,7 @@ golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 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= diff --git a/example/http/images/JaegarTraceExample.png b/example/http/images/JaegarTraceExample.png new file mode 100644 index 000000000..5d866bb2d Binary files /dev/null and b/example/http/images/JaegarTraceExample.png differ diff --git a/example/http/server/server.go b/example/http/server/server.go index 0a3d099de..8622ec165 100644 --- a/example/http/server/server.go +++ b/example/http/server/server.go @@ -16,24 +16,43 @@ package main import ( "io" + "log" "net/http" - "go.opentelemetry.io/api/key" "go.opentelemetry.io/api/tag" "go.opentelemetry.io/api/trace" + "go.opentelemetry.io/exporter/trace/jaeger" "go.opentelemetry.io/plugin/httptrace" + sdktrace "go.opentelemetry.io/sdk/trace" ) -var ( - tracer = trace.GlobalTracer(). - WithService("server"). - WithComponent("main"). - WithResources( - key.New("whatevs").String("nooooo"), - ) -) +func initTracer() { + sdktrace.Register() + + // Create Jaeger exporter to be able to retrieve + // the collected spans. + exporter, err := jaeger.NewExporter(jaeger.Options{ + CollectorEndpoint: "http://localhost:14268/api/traces", + Process: jaeger.Process{ + ServiceName: "trace-demo", + }, + }) + if err != nil { + log.Fatal(err) + } + + // Wrap Jaeger exporter with SimpleSpanProcessor and register the processor. + ssp := sdktrace.NewSimpleSpanProcessor(exporter) + sdktrace.RegisterSpanProcessor(ssp) + + // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. + // In a production application, use sdktrace.ProbabilitySampler with a desired probability. + sdktrace.ApplyConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}) +} func main() { + initTracer() + helloHandler := func(w http.ResponseWriter, req *http.Request) { attrs, tags, spanCtx := httptrace.Extract(req.Context(), req) @@ -41,7 +60,7 @@ func main() { MultiKV: tags, }))) - ctx, span := tracer.Start( + ctx, span := trace.GlobalTracer().Start( req.Context(), "hello", trace.WithAttributes(attrs...), diff --git a/exporter/trace/jaeger/jaeger.go b/exporter/trace/jaeger/jaeger.go index 4ddeb3bd7..d0c1bdd6d 100644 --- a/exporter/trace/jaeger/jaeger.go +++ b/exporter/trace/jaeger/jaeger.go @@ -22,13 +22,12 @@ import ( "io/ioutil" "log" "net/http" - "time" - - "google.golang.org/grpc/codes" "github.com/apache/thrift/lib/go/thrift" "google.golang.org/api/support/bundler" + "google.golang.org/grpc/codes" + "go.opentelemetry.io/api/core" gen "go.opentelemetry.io/exporter/trace/jaeger/internal/gen-go/jaeger" "go.opentelemetry.io/sdk/trace" ) @@ -165,38 +164,33 @@ func (e *Exporter) ExportSpan(data *trace.SpanData) { func spanDataToThrift(data *trace.SpanData) *gen.Span { tags := make([]*gen.Tag, 0, len(data.Attributes)) - for k, v := range data.Attributes { - tag := attributeToTag(k, v) - if tag != nil { - tags = append(tags, tag) - } + for _, kv := range data.Attributes { + tag := coreAttributeToTag(kv) + tags = append(tags, tag) } - tags = append(tags, - attributeToTag("status.code", int32(data.Status)), - attributeToTag("status.message", data.Status.String()), + tags = append(tags, getInt64Tag("status.code", int64(data.Status)), + getStringTag("status.message", data.Status.String()), ) // Ensure that if Status.Code is not OK, that we set the "error" tag on the Jaeger span. // See Issue https://github.com/census-instrumentation/opencensus-go/issues/1041 if data.Status != codes.OK { - tags = append(tags, attributeToTag("error", true)) + tags = append(tags, getBoolTag("error", true)) } var logs []*gen.Log for _, a := range data.MessageEvents { fields := make([]*gen.Tag, 0, len(a.Attributes)) for _, kv := range a.Attributes { - tag := attributeToTag(kv.Key.Name, kv.Value.Emit()) + tag := coreAttributeToTag(kv) if tag != nil { fields = append(fields, tag) } } - fields = append(fields, attributeToTag("message", a.Message)) + fields = append(fields, getStringTag("message", a.Message)) logs = append(logs, &gen.Log{ - //Timestamp: a.Time.UnixNano() / 1000, - //TODO: [rghetia] update when time is supported in the event. - Timestamp: time.Now().UnixNano() / 1000, + Timestamp: a.Time.UnixNano() / 1000, Fields: fields, }) } @@ -227,6 +221,61 @@ func spanDataToThrift(data *trace.SpanData) *gen.Span { } } +func coreAttributeToTag(kv core.KeyValue) *gen.Tag { + var tag *gen.Tag + switch kv.Value.Type { + case core.STRING: + tag = &gen.Tag{ + Key: kv.Key.Name, + VStr: &kv.Value.String, + VType: gen.TagType_STRING, + } + case core.BOOL: + tag = &gen.Tag{ + Key: kv.Key.Name, + VBool: &kv.Value.Bool, + VType: gen.TagType_BOOL, + } + case core.INT32, core.INT64: + tag = &gen.Tag{ + Key: kv.Key.Name, + VLong: &kv.Value.Int64, + VType: gen.TagType_LONG, + } + case core.FLOAT32, core.FLOAT64: + tag = &gen.Tag{ + Key: kv.Key.Name, + VDouble: &kv.Value.Float64, + VType: gen.TagType_DOUBLE, + } + } + return tag +} + +func getInt64Tag(k string, i int64) *gen.Tag { + return &gen.Tag{ + Key: k, + VLong: &i, + VType: gen.TagType_LONG, + } +} + +func getStringTag(k, s string) *gen.Tag { + return &gen.Tag{ + Key: k, + VStr: &s, + VType: gen.TagType_STRING, + } +} + +func getBoolTag(k string, b bool) *gen.Tag { + return &gen.Tag{ + Key: k, + VBool: &b, + VType: gen.TagType_BOOL, + } +} + // TODO(rghetia): remove interface{}. see https://github.com/open-telemetry/opentelemetry-go/pull/112/files#r321444786 func attributeToTag(key string, a interface{}) *gen.Tag { var tag *gen.Tag diff --git a/exporter/trace/jaeger/jaeger_test.go b/exporter/trace/jaeger/jaeger_test.go index 24729d21e..256041bfe 100644 --- a/exporter/trace/jaeger/jaeger_test.go +++ b/exporter/trace/jaeger/jaeger_test.go @@ -55,9 +55,15 @@ func Test_spanDataToThrift(t *testing.T) { Name: "/foo", StartTime: now, EndTime: now, - Attributes: map[string]interface{}{ - "double": doubleValue, - "key": keyValue, + Attributes: []core.KeyValue{ + { + Key: core.Key{Name: "key"}, + Value: core.Value{Type: core.STRING, String: keyValue}, + }, + { + Key: core.Key{Name: "double"}, + Value: core.Value{Type: core.FLOAT64, Float64: doubleValue}, + }, }, // TODO: [rghetia] add events test after event is concrete type. Status: codes.Unknown, diff --git a/sdk/trace/export.go b/sdk/trace/export.go index c3b80511b..543b22ca2 100644 --- a/sdk/trace/export.go +++ b/sdk/trace/export.go @@ -86,7 +86,7 @@ type SpanData struct { // from StartTime by the duration of the span. EndTime time.Time // The values of Attributes each have type string, bool, or int64. - Attributes map[string]interface{} + Attributes []core.KeyValue MessageEvents []Event Links []apitrace.Link Status codes.Code diff --git a/sdk/trace/span.go b/sdk/trace/span.go index f0347f948..dc29e3b38 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -272,13 +272,14 @@ func (s *span) interfaceArrayToMessageEventArray() []Event { return messageEventArr } -func (s *span) lruAttributesToAttributeMap() map[string]interface{} { - attributes := make(map[string]interface{}) +func (s *span) lruAttributesToAttributeMap() []core.KeyValue { + attributes := make([]core.KeyValue, 0, s.lruAttributes.simpleLruMap.Len()) for _, key := range s.lruAttributes.simpleLruMap.Keys() { value, ok := s.lruAttributes.simpleLruMap.Get(key) if ok { key := key.(core.Key) - attributes[key.Name] = value + value := value.(core.Value) + attributes = append(attributes, core.KeyValue{Key: key, Value: value}) } } return attributes diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 1268878f9..2461b706c 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -186,9 +186,12 @@ func TestSetSpanAttributes(t *testing.T) { TraceID: tid, TraceOptions: 0x1, }, - ParentSpanID: sid, - Name: "span0", - Attributes: map[string]interface{}{"key1": core.Value{Type: core.STRING, String: "value1"}}, + ParentSpanID: sid, + Name: "span0", + Attributes: []core.KeyValue{{ + Key: core.Key{Name: "key1"}, + Value: core.Value{Type: core.STRING, String: "value1"}, + }}, HasRemoteParent: true, } if diff := cmp.Diff(got, want); diff != "" { @@ -217,9 +220,16 @@ func TestSetSpanAttributesOverLimit(t *testing.T) { }, ParentSpanID: sid, Name: "span0", - Attributes: map[string]interface{}{ - "key1": core.Value{Type: core.STRING, String: "value3"}, - "key4": core.Value{Type: core.STRING, String: "value4"}}, + Attributes: []core.KeyValue{ + { + Key: core.Key{Name: "key1"}, + Value: core.Value{Type: core.STRING, String: "value3"}, + }, + { + Key: core.Key{Name: "key4"}, + Value: core.Value{Type: core.STRING, String: "value4"}, + }, + }, HasRemoteParent: true, DroppedAttributeCount: 1, }