From 16ed1f8b2eec7e0a7b2b07487e13a8616ebdfc22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:20:22 +0000 Subject: [PATCH] Implement OpenTelemetry V2.8 improvements: configurable SQL, request, and response tracing Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> --- database/gdb/gdb_core_config.go | 4 + database/gdb/gdb_core_trace.go | 7 + .../gdb_z_unit_feature_otel_tracing_test.go | 73 ++++++++++ net/ghttp/ghttp_middleware_tracing.go | 62 +++++++- net/ghttp/ghttp_server_config.go | 14 +- .../ghttp_z_unit_feature_otel_tracing_test.go | 137 ++++++++++++++++++ 6 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 database/gdb/gdb_z_unit_feature_otel_tracing_test.go create mode 100644 net/ghttp/ghttp_z_unit_feature_otel_tracing_test.go diff --git a/database/gdb/gdb_core_config.go b/database/gdb/gdb_core_config.go index ae9c7c85b..185485046 100644 --- a/database/gdb/gdb_core_config.go +++ b/database/gdb/gdb_core_config.go @@ -66,6 +66,10 @@ type ConfigNode struct { // Optional field Debug bool `json:"debug"` + // OtelTraceSQLEnabled enables OpenTelemetry tracing for SQL operations + // Optional field + OtelTraceSQLEnabled bool `json:"otelTraceSQLEnabled"` + // Prefix specifies the table name prefix // Optional field Prefix string `json:"prefix"` diff --git a/database/gdb/gdb_core_trace.go b/database/gdb/gdb_core_trace.go index 8b24f0c67..7a2bc9ec9 100644 --- a/database/gdb/gdb_core_trace.go +++ b/database/gdb/gdb_core_trace.go @@ -33,6 +33,7 @@ const ( traceEventDbExecutionRows = "db.execution.rows" traceEventDbExecutionTxID = "db.execution.txid" traceEventDbExecutionType = "db.execution.type" + traceEventDbExecutionSQL = "db.execution.sql" ) // addSqlToTracing adds sql information to tracer if it's enabled. @@ -80,5 +81,11 @@ func (c *Core) traceSpanEnd(ctx context.Context, span trace.Span, sql *Sql) { } } events = append(events, attribute.String(traceEventDbExecutionType, string(sql.Type))) + + // Add SQL statement to tracing if enabled + if c.db.GetConfig().OtelTraceSQLEnabled { + events = append(events, attribute.String(traceEventDbExecutionSQL, sql.Format)) + } + span.AddEvent(traceEventDbExecution, trace.WithAttributes(events...)) } diff --git a/database/gdb/gdb_z_unit_feature_otel_tracing_test.go b/database/gdb/gdb_z_unit_feature_otel_tracing_test.go new file mode 100644 index 000000000..861f0eebe --- /dev/null +++ b/database/gdb/gdb_z_unit_feature_otel_tracing_test.go @@ -0,0 +1,73 @@ +// 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 gdb_test + +import ( + "testing" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/test/gtest" +) + +func Test_OTEL_SQLTracing_Default(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + config := gdb.ConfigNode{ + Type: "sqlite", + Name: ":memory:", + } + + // By default, SQL tracing should be disabled + t.Assert(config.OtelTraceSQLEnabled, false) + }) +} + +func Test_OTEL_SQLTracing_Configuration(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + config := gdb.ConfigNode{ + Type: "sqlite", + Name: ":memory:", + OtelTraceSQLEnabled: true, + } + + // SQL tracing should be configurable + t.Assert(config.OtelTraceSQLEnabled, true) + }) +} + +func Test_OTEL_SQLTracing_Enabled(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + config := gdb.ConfigNode{ + Type: "mysql", + Name: "test_db", + OtelTraceSQLEnabled: true, + } + + // Test that the configuration field can be set and retrieved + t.Assert(config.OtelTraceSQLEnabled, true) + + // Test that the field is preserved during configuration operations + configGroup := gdb.ConfigGroup{config} + t.Assert(configGroup[0].OtelTraceSQLEnabled, true) + }) +} + +func Test_OTEL_SQLTracing_Disabled(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + config := gdb.ConfigNode{ + Type: "mysql", + Name: "test_db", + OtelTraceSQLEnabled: false, + } + + // Test that the configuration field can be set and retrieved + t.Assert(config.OtelTraceSQLEnabled, false) + + // Test that the field is preserved during configuration operations + configGroup := gdb.ConfigGroup{config} + t.Assert(configGroup[0].OtelTraceSQLEnabled, false) + }) +} \ No newline at end of file diff --git a/net/ghttp/ghttp_middleware_tracing.go b/net/ghttp/ghttp_middleware_tracing.go index 8e1559153..59ace2eea 100644 --- a/net/ghttp/ghttp_middleware_tracing.go +++ b/net/ghttp/ghttp_middleware_tracing.go @@ -20,6 +20,7 @@ import ( "github.com/gogf/gf/v2/internal/httputil" "github.com/gogf/gf/v2/net/gtrace" "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" ) @@ -28,9 +29,12 @@ const ( tracingEventHttpRequest = "http.request" tracingEventHttpRequestHeaders = "http.request.headers" tracingEventHttpRequestBaggage = "http.request.baggage" + tracingEventHttpRequestParams = "http.request.params" tracingEventHttpResponse = "http.response" tracingEventHttpResponseHeaders = "http.response.headers" + tracingEventHttpResponseBody = "http.response.body" tracingEventHttpRequestUrl = "http.request.url" + tracingEventHttpMethod = "http.method" tracingMiddlewareHandled gctx.StrKey = `MiddlewareServerTracingHandled` ) @@ -75,11 +79,44 @@ func internalMiddlewareServerTracing(r *Request) { return } - span.AddEvent(tracingEventHttpRequest, trace.WithAttributes( + // Basic trace attributes for all requests + traceAttrs := []attribute.KeyValue{ attribute.String(tracingEventHttpRequestUrl, r.URL.String()), + attribute.String(tracingEventHttpMethod, r.Method), attribute.String(tracingEventHttpRequestHeaders, gconv.String(httputil.HeaderToMap(r.Header))), attribute.String(tracingEventHttpRequestBaggage, gtrace.GetBaggageMap(ctx).String()), - )) + } + + // Add request parameters if configured + if r.Server != nil && r.Server.config.OtelTraceRequestEnabled { + // Get all request parameters (query + form + body) + requestParams := make(map[string]any) + + // Query parameters + for k, v := range r.URL.Query() { + requestParams[k] = v + } + + // Form parameters + if r.ContentLength > 0 && gtrace.MaxContentLogSize() > 0 { + contentType := r.Header.Get("Content-Type") + if gstr.Contains(contentType, "application/x-www-form-urlencoded") || gstr.Contains(contentType, "multipart/form-data") { + // Use GetFormMap() instead of ParseForm() to get form data + formData := r.GetFormMap() + for k, v := range formData { + requestParams[k] = v + } + } + } + + if len(requestParams) > 0 { + traceAttrs = append(traceAttrs, + attribute.String(tracingEventHttpRequestParams, gconv.String(requestParams)), + ) + } + } + + span.AddEvent(tracingEventHttpRequest, trace.WithAttributes(traceAttrs...)) // Continue executing. r.Middleware.Next() @@ -94,10 +131,27 @@ func internalMiddlewareServerTracing(r *Request) { span.SetStatus(codes.Error, fmt.Sprintf(`%+v`, err)) } - span.AddEvent(tracingEventHttpResponse, trace.WithAttributes( + // Response tracing attributes + responseAttrs := []attribute.KeyValue{ attribute.String( tracingEventHttpResponseHeaders, gconv.String(httputil.HeaderToMap(r.Response.Header())), ), - )) + } + + // Add response body if configured + if r.Server != nil && r.Server.config.OtelTraceResponseEnabled { + if r.Response.BufferLength() > 0 { + responseBody := r.Response.BufferString() + // Limit response body size for tracing to avoid memory issues + if len(responseBody) > gtrace.MaxContentLogSize() { + responseBody = responseBody[:gtrace.MaxContentLogSize()] + "...[truncated]" + } + responseAttrs = append(responseAttrs, + attribute.String(tracingEventHttpResponseBody, responseBody), + ) + } + } + + span.AddEvent(tracingEventHttpResponse, trace.WithAttributes(responseAttrs...)) } diff --git a/net/ghttp/ghttp_server_config.go b/net/ghttp/ghttp_server_config.go index 8ce6f4266..2c365e28b 100644 --- a/net/ghttp/ghttp_server_config.go +++ b/net/ghttp/ghttp_server_config.go @@ -243,6 +243,16 @@ type ServerConfig struct { // GracefulShutdownTimeout set the maximum survival time (seconds) before stopping the server. GracefulShutdownTimeout int `json:"gracefulShutdownTimeout"` + // ====================================================================================================== + // OpenTelemetry Tracing. + // ====================================================================================================== + + // OtelTraceRequestEnabled enables tracing of HTTP request parameters. + OtelTraceRequestEnabled bool `json:"otelTraceRequestEnabled"` + + // OtelTraceResponseEnabled enables tracing of HTTP response parameters. + OtelTraceResponseEnabled bool `json:"otelTraceResponseEnabled"` + // ====================================================================================================== // Other. // ====================================================================================================== @@ -305,7 +315,9 @@ func NewConfig() ServerConfig { ErrorLogEnabled: true, ErrorLogPattern: "error-{Ymd}.log", AccessLogEnabled: false, - AccessLogPattern: "access-{Ymd}.log", + AccessLogPattern: "access-{Ymd}.log", + OtelTraceRequestEnabled: false, + OtelTraceResponseEnabled: false, DumpRouterMap: true, ClientMaxBodySize: 8 * 1024 * 1024, // 8MB FormParsingMemory: 1024 * 1024, // 1MB diff --git a/net/ghttp/ghttp_z_unit_feature_otel_tracing_test.go b/net/ghttp/ghttp_z_unit_feature_otel_tracing_test.go new file mode 100644 index 000000000..9483c0e4e --- /dev/null +++ b/net/ghttp/ghttp_z_unit_feature_otel_tracing_test.go @@ -0,0 +1,137 @@ +// 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 ghttp_test + +import ( + "fmt" + "testing" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_OTEL_RequestTracing_Disabled(t *testing.T) { + s := g.Server(guid.S()) + s.BindHandler("/test", func(r *ghttp.Request) { + r.Response.WriteJson(g.Map{"result": "ok"}) + }) + s.SetDumpRouterMap(false) + + // By default, request tracing should be disabled + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + prefix := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + client := g.Client() + client.SetPrefix(prefix) + res, err := client.Post(ctx, "/test", g.Map{"param1": "value1"}) + t.AssertNil(err) + defer res.Close() + + t.Assert(res.StatusCode, 200) + }) +} + +func Test_OTEL_RequestTracing_Enabled(t *testing.T) { + s := g.Server(guid.S()) + + // Enable request tracing using SetConfigWithMap + err := s.SetConfigWithMap(g.Map{ + "OtelTraceRequestEnabled": true, + }) + gtest.AssertNil(err) + + s.BindHandler("/test", func(r *ghttp.Request) { + r.Response.WriteJson(g.Map{"result": "ok"}) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + prefix := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + client := g.Client() + client.SetPrefix(prefix) + res, err := client.Post(ctx, "/test?query1=qvalue1", g.Map{"param1": "value1"}) + t.AssertNil(err) + defer res.Close() + + t.Assert(res.StatusCode, 200) + // Test passes if no errors occurred during tracing + }) +} + +func Test_OTEL_ResponseTracing_Enabled(t *testing.T) { + s := g.Server(guid.S()) + + // Enable response tracing using SetConfigWithMap + err := s.SetConfigWithMap(g.Map{ + "OtelTraceResponseEnabled": true, + }) + gtest.AssertNil(err) + + s.BindHandler("/test", func(r *ghttp.Request) { + r.Response.WriteJson(g.Map{"result": "success", "data": "test data"}) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + prefix := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + client := g.Client() + client.SetPrefix(prefix) + res, err := client.Get(ctx, "/test") + t.AssertNil(err) + defer res.Close() + + t.Assert(res.StatusCode, 200) + // Test passes if no errors occurred during response tracing + }) +} + +func Test_OTEL_BothTracingEnabled(t *testing.T) { + s := g.Server(guid.S()) + + // Enable both request and response tracing using SetConfigWithMap + err := s.SetConfigWithMap(g.Map{ + "OtelTraceRequestEnabled": true, + "OtelTraceResponseEnabled": true, + }) + gtest.AssertNil(err) + + s.BindHandler("/test", func(r *ghttp.Request) { + r.Response.WriteJson(g.Map{ + "received_param": r.Get("param1"), + "received_query": r.Get("query1"), + "result": "success", + }) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + prefix := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()) + client := g.Client() + client.SetPrefix(prefix) + res, err := client.Post(ctx, "/test?query1=testquery", g.Map{"param1": "testparam"}) + t.AssertNil(err) + defer res.Close() + + t.Assert(res.StatusCode, 200) + // Test passes if no errors occurred during both request and response tracing + }) +} \ No newline at end of file