add tracing middleware for ghttp.Client/Server; add package gtrace

This commit is contained in:
jianchenma
2021-01-25 18:43:47 +08:00
parent 3f2ae3ba62
commit cc1e340585
8 changed files with 257 additions and 117 deletions

View File

@ -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...
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

48
net/gtrace/gtrace.go Normal file
View File

@ -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())
}