mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
add tracing middleware for ghttp.Client/Server; add package gtrace
This commit is contained in:
@ -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...
|
||||
}
|
||||
46
.example/net/gtrace/1.http/client/main.go
Normal file
46
.example/net/gtrace/1.http/client/main.go
Normal 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)
|
||||
}
|
||||
52
.example/net/gtrace/1.http/server/main.go
Normal file
52
.example/net/gtrace/1.http/server/main.go
Normal 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())
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
48
net/gtrace/gtrace.go
Normal 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())
|
||||
}
|
||||
Reference in New Issue
Block a user