diff --git a/.example/net/ghttp/client/tracing/jaeger/main.go b/.example/net/ghttp/client/tracing/jaeger/main.go deleted file mode 100644 index c845c8307..000000000 --- a/.example/net/ghttp/client/tracing/jaeger/main.go +++ /dev/null @@ -1,91 +0,0 @@ -// 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. - -// Command jaeger is an example program that creates spans -// and uploads to Jaeger. -package main - -import ( - "context" - "fmt" - "github.com/gogf/gf/frame/g" - "github.com/gogf/gf/net/ghttp" - "go.opentelemetry.io/otel/exporters/trace/jaeger" - "go.opentelemetry.io/otel/trace" - "log" - "time" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/label" - - sdktrace "go.opentelemetry.io/otel/sdk/trace" -) - -// initTracer creates a new trace provider instance and registers it as global trace provider. -func initTracer() func() { - // Create and install Jaeger export pipeline. - flush, err := jaeger.InstallNewPipeline( - jaeger.WithCollectorEndpoint("http://localhost:14268/api/traces"), - jaeger.WithProcess(jaeger.Process{ - ServiceName: "http-trace-demo", - Tags: []label.KeyValue{ - label.String("exporter", "jaeger"), - label.Float64("float", 312.23), - }, - }), - jaeger.WithSDK(&sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), - ) - if err != nil { - log.Fatal(err) - } - return flush -} - -func main() { - ctx := context.Background() - - flush := initTracer() - defer flush() - - ctx, span := otel.Tracer("test").Start(ctx, "test") - defer span.End() - - for i := 0; i < 20; i++ { - g.Client().Use(ghttp.MiddlewareClientTracing).Ctx(ctx).Header(g.MapStrStr{ - "test": "123", - "john": "smith", - }).Cookie(g.MapStrStr{ - "cookieKey": "cookieValue", - }).GetContent(fmt.Sprintf("http://baidu.com/?q=test_%d", i)) - } - foo(ctx) -} - -func foo(ctx context.Context) { - ctx, span := otel.Tracer("test").Start(ctx, "foo") - defer span.End() - span.AddEvent("Nice operation!", trace.WithAttributes(label.Int("bogons", 100))) - span.SetAttributes(label.String("test2", "123")) - time.Sleep(time.Second * 1) - bar(ctx) -} - -func bar(ctx context.Context) { - _, span := otel.Tracer("test").Start(ctx, "bar") - defer span.End() - span.AddEvent("Nice operation!", trace.WithAttributes(label.Int("bogons", 100))) - span.SetAttributes(label.String("test2", "123")) - time.Sleep(time.Second * 1) - // Do bar... -} diff --git a/.example/net/gtrace/1.http/client/main.go b/.example/net/gtrace/1.http/client/main.go new file mode 100644 index 000000000..cae43e329 --- /dev/null +++ b/.example/net/gtrace/1.http/client/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/net/gtrace" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/exporters/trace/jaeger" + "go.opentelemetry.io/otel/label" + sdkTrace "go.opentelemetry.io/otel/sdk/trace" +) + +const ( + JaegerEndpoint = "http://localhost:14268/api/traces" + ServiceName = "TracingHttpClient" +) + +// initTracer creates a new trace provider instance and registers it as global trace provider. +func initTracer() func() { + // Create and install Jaeger export pipeline. + flush, err := jaeger.InstallNewPipeline( + jaeger.WithCollectorEndpoint(JaegerEndpoint), + jaeger.WithProcess(jaeger.Process{ + ServiceName: ServiceName, + }), + jaeger.WithSDK(&sdkTrace.Config{DefaultSampler: sdkTrace.AlwaysSample()}), + ) + if err != nil { + g.Log().Fatal(err) + } + return flush +} + +func main() { + flush := initTracer() + defer flush() + + ctx, span := gtrace.Tracer().Start(context.Background(), "test") + defer span.End() + + ctx = baggage.ContextWithValues(ctx, label.String("name", "john")) + client := g.Client().Use(ghttp.MiddlewareClientTracing) + content := client.Ctx(ctx).GetContent("http://127.0.0.1:8199/hello") + g.Log().Print(content) +} diff --git a/.example/net/gtrace/1.http/server/main.go b/.example/net/gtrace/1.http/server/main.go new file mode 100644 index 000000000..1be9b6fc8 --- /dev/null +++ b/.example/net/gtrace/1.http/server/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/net/gtrace" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/exporters/trace/jaeger" + sdkTrace "go.opentelemetry.io/otel/sdk/trace" +) + +const ( + JaegerEndpoint = "http://localhost:14268/api/traces" + ServiceName = "TracingHttpServer" +) + +// initTracer creates a new trace provider instance and registers it as global trace provider. +func initTracer() func() { + // Create and install Jaeger export pipeline. + flush, err := jaeger.InstallNewPipeline( + jaeger.WithCollectorEndpoint(JaegerEndpoint), + jaeger.WithProcess(jaeger.Process{ + ServiceName: ServiceName, + }), + jaeger.WithSDK(&sdkTrace.Config{DefaultSampler: sdkTrace.AlwaysSample()}), + ) + if err != nil { + g.Log().Fatal(err) + } + return flush +} + +func main() { + flush := initTracer() + defer flush() + + s := g.Server() + s.Group("/", func(group *ghttp.RouterGroup) { + group.Middleware(ghttp.MiddlewareServerTracing) + group.GET("/hello", helloHandler) + }) + s.SetPort(8199) + s.Run() +} + +func helloHandler(r *ghttp.Request) { + ctx, span := gtrace.Tracer().Start(r.Context(), "helloHandler") + defer span.End() + + value := baggage.Value(ctx, "name") + r.Response.Write("hello:", value.AsString()) +} diff --git a/net/ghttp/ghttp_middleware.go b/net/ghttp/ghttp_middleware.go index 9531ca378..3460f46ee 100644 --- a/net/ghttp/ghttp_middleware.go +++ b/net/ghttp/ghttp_middleware.go @@ -7,10 +7,81 @@ package ghttp import ( + "fmt" + "github.com/gogf/gf" + "github.com/gogf/gf/internal/utils" "github.com/gogf/gf/net/ghttp/internal/client" + "github.com/gogf/gf/net/ghttp/internal/httputil" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/label" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "io/ioutil" "net/http" ) +const ( + tracingMaxContentLogSize = 512 * 1024 // Max log size for request and response body. +) + +// MiddlewareClientTracing is a client middleware that enables tracing feature using standards of OpenTelemetry. func MiddlewareClientTracing(c *Client, r *http.Request) (*ClientResponse, error) { return client.MiddlewareTracing(c, r) } + +// MiddlewareServerTracing is a serer middleware that enables tracing feature using standards of OpenTelemetry. +func MiddlewareServerTracing(r *Request) { + tr := otel.GetTracerProvider().Tracer( + "github.com/gogf/gf/net/ghttp.Server", + trace.WithInstrumentationVersion(fmt.Sprintf(`%s`, gf.VERSION)), + ) + // Tracing content parsing, start root span. + propagator := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + ctx := propagator.Extract(r.Context(), r.Header) + ctx, span := tr.Start(ctx, r.URL.String()) + defer span.End() + + // Inject tracing context. + r.SetCtx(ctx) + + // Request content logging. + var reqBodyContent string + if r.ContentLength <= tracingMaxContentLogSize { + reqBodyContentBytes, _ := ioutil.ReadAll(r.Body) + r.Body = utils.NewReadCloser(reqBodyContentBytes, false) + reqBodyContent = string(reqBodyContentBytes) + } else { + reqBodyContent = fmt.Sprintf( + "[Request Body Too Large For Logging, Max: %d bytes]", + tracingMaxContentLogSize, + ) + } + span.AddEvent("http.request", trace.WithAttributes( + label.Any(`http.request.headers`, httputil.HeaderToMap(r.Header)), + label.String(`http.request.body`, reqBodyContent), + )) + + // Continue executing. + r.Middleware.Next() + + // Error logging. + if err := r.GetError(); err != nil { + span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, err)) + } + // Response content logging. + var resBodyContent string + if r.Response.BufferLength() <= tracingMaxContentLogSize { + resBodyContent = r.Response.BufferString() + } else { + resBodyContent = fmt.Sprintf("[Response Body Too Large For Logging, Max: %d bytes]", tracingMaxContentLogSize) + } + span.AddEvent("http.response", trace.WithAttributes( + label.Any(`http.response.headers`, httputil.HeaderToMap(r.Response.Header())), + label.String(`http.response.body`, resBodyContent), + )) + return +} diff --git a/net/ghttp/internal/client/client_tracing.go b/net/ghttp/internal/client/client_tracing.go index d5c9a91b2..546447fa8 100644 --- a/net/ghttp/internal/client/client_tracing.go +++ b/net/ghttp/internal/client/client_tracing.go @@ -10,9 +10,11 @@ import ( "fmt" "github.com/gogf/gf" "github.com/gogf/gf/internal/utils" + "github.com/gogf/gf/net/ghttp/internal/httputil" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/label" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" "io/ioutil" "net/http" @@ -20,7 +22,7 @@ import ( ) const ( - maxContentLogSize = 512 * 1024 // Max log size for request and response body. + tracingMaxContentLogSize = 512 * 1024 // Max log size for request and response body. ) // MiddlewareTracing is a client middleware that enables tracing feature using standards of OpenTelemetry. @@ -31,6 +33,15 @@ func MiddlewareTracing(c *Client, r *http.Request) (response *Response, err erro ) ctx, span := tr.Start(r.Context(), r.URL.String()) defer span.End() + + // Inject tracing content into http header. + propagator := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + propagator.Inject(ctx, r.Header) + + // Continue client handler executing. response, err = c.Next( r.WithContext( httptrace.WithClientTrace( @@ -41,32 +52,21 @@ func MiddlewareTracing(c *Client, r *http.Request) (response *Response, err erro if err != nil { span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, err)) } + if response == nil { + return + } var resBodyContent string - if response.ContentLength <= maxContentLogSize { + if response.ContentLength <= tracingMaxContentLogSize { reqBodyContentBytes, _ := ioutil.ReadAll(response.Body) resBodyContent = string(reqBodyContentBytes) response.Body = utils.NewReadCloser(reqBodyContentBytes, false) } else { - resBodyContent = fmt.Sprintf("[Response Body Too Large For Logging, Max: %d bytes]", maxContentLogSize) - } - if response != nil { - span.AddEvent("http.response", trace.WithAttributes( - label.Any(`http.response.headers`, headerToMap(response.Header)), - label.String(`http.response.body`, resBodyContent), - )) + resBodyContent = fmt.Sprintf("[Response Body Too Large For Logging, Max: %d bytes]", tracingMaxContentLogSize) } + + span.AddEvent("http.response", trace.WithAttributes( + label.Any(`http.response.headers`, httputil.HeaderToMap(response.Header)), + label.String(`http.response.body`, resBodyContent), + )) return } - -// headerToMap coverts request headers to map. -func headerToMap(header http.Header) map[string]interface{} { - m := make(map[string]interface{}) - for k, v := range header { - if len(v) > 1 { - m[k] = v - } else { - m[k] = v[0] - } - } - return m -} diff --git a/net/ghttp/internal/client/client_tracing_tracer.go b/net/ghttp/internal/client/client_tracing_tracer.go index 154eb2473..fa2f6366e 100644 --- a/net/ghttp/internal/client/client_tracing_tracer.go +++ b/net/ghttp/internal/client/client_tracing_tracer.go @@ -131,7 +131,7 @@ func (ct *clientTracer) wroteHeaderField(k string, v []string) { } func (ct *clientTracer) wroteHeaders() { - if ct.request.ContentLength <= maxContentLogSize { + if ct.request.ContentLength <= tracingMaxContentLogSize { reqBodyContent, _ := ioutil.ReadAll(ct.request.Body) ct.requestBody = reqBodyContent ct.request.Body = utils.NewReadCloser(reqBodyContent, false) @@ -143,10 +143,10 @@ func (ct *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) { ct.span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, info.Err)) } var bodyContent string - if ct.request.ContentLength <= maxContentLogSize { + if ct.request.ContentLength <= tracingMaxContentLogSize { bodyContent = string(ct.requestBody) } else { - bodyContent = fmt.Sprintf("[Request Body Too Large For Logging, Max: %d bytes]", maxContentLogSize) + bodyContent = fmt.Sprintf("[Request Body Too Large For Logging, Max: %d bytes]", tracingMaxContentLogSize) } ct.span.AddEvent("http.request", trace.WithAttributes( label.Any(`http.request.headers`, ct.headers), @@ -163,7 +163,7 @@ func (ct *clientTracer) wait100Continue() { } func (ct *clientTracer) gotFirstResponseByte() { - ct.span.AddEvent("http.request.receive", trace.WithAttributes()) + } func (ct *clientTracer) got1xxResponse(code int, header textproto.MIMEHeader) error { diff --git a/net/ghttp/internal/httputil/httputils.go b/net/ghttp/internal/httputil/httputils.go index 209acb782..f7ea4db35 100644 --- a/net/ghttp/internal/httputil/httputils.go +++ b/net/ghttp/internal/httputil/httputils.go @@ -8,6 +8,7 @@ package httputil import ( "github.com/gogf/gf/text/gstr" + "net/http" "strings" "github.com/gogf/gf/encoding/gurl" @@ -64,3 +65,16 @@ func BuildParams(params interface{}, noUrlEncode ...bool) (encodedParamStr strin } return } + +// HeaderToMap coverts request headers to map. +func HeaderToMap(header http.Header) map[string]interface{} { + m := make(map[string]interface{}) + for k, v := range header { + if len(v) > 1 { + m[k] = v + } else { + m[k] = v[0] + } + } + return m +} diff --git a/net/gtrace/gtrace.go b/net/gtrace/gtrace.go new file mode 100644 index 000000000..232951004 --- /dev/null +++ b/net/gtrace/gtrace.go @@ -0,0 +1,48 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +// Package gtrace provides convenience wrapping functionality for tracing feature using OpenTelemetry. +package gtrace + +import ( + "context" + "github.com/gogf/gf/container/gvar" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/label" + "go.opentelemetry.io/otel/trace" +) + +// Tracer is a short function for retrieve Tracer. +func Tracer(name ...string) trace.Tracer { + tracerName := "" + if len(name) > 0 { + tracerName = name[0] + } + return otel.Tracer(tracerName) +} + +// SetBaggageValue is a convenient function for adding one key-value pair to baggage. +// Note that it uses label.Any to set the key-value pair. +func SetBaggageValue(ctx context.Context, key string, value interface{}) context.Context { + return baggage.ContextWithValues(ctx, label.Any(key, value)) +} + +// SetBaggageMap is a convenient function for adding map key-value pairs to baggage. +// Note that it uses label.Any to set the key-value pair. +func SetBaggageMap(ctx context.Context, data map[string]interface{}) context.Context { + pairs := make([]label.KeyValue, 0) + for k, v := range data { + pairs = append(pairs, label.Any(k, v)) + } + return baggage.ContextWithValues(ctx, pairs...) +} + +// GetBaggageVar retrieves value and returns a *gvar.Var for specified key from baggage. +func GetBaggageVar(ctx context.Context, key string) *gvar.Var { + value := baggage.Value(ctx, label.Key(key)) + return gvar.New(value.AsInterface()) +}