This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/skywalking-go.git
The following commit(s) were added to refs/heads/main by this push: new 15f2a54 feature: Support http headers collection for Gin (#178) 15f2a54 is described below commit 15f2a54ffa1f10e22728f609073592cc554c7a24 Author: IceSoda177 <dylanwu...@foxmail.com> AuthorDate: Mon Apr 1 11:06:54 2024 +0800 feature: Support http headers collection for Gin (#178) --- CHANGES.md | 1 + docs/en/agent/plugin-configurations.md | 2 + plugins/core/operator/tools.go | 1 + plugins/core/tools/strconv.go | 11 ++++++ plugins/core/tracer_tools.go | 5 +++ plugins/core/tracing/span.go | 1 + plugins/{core/operator/tools.go => gin/config.go} | 14 +++---- plugins/gin/intercepter.go | 33 +++++++++++++++- plugins/gin/intercepter_test.go | 44 +++++++++++++++++++++- test/plugins/scenarios/gin/bin/startup.sh | 3 ++ test/plugins/scenarios/gin/config/excepted.yml | 1 + test/plugins/scenarios/gin/main.go | 14 ++++++- tools/go-agent/config/agent.default.yaml | 5 +++ .../go-agent/instrument/plugins/enhance_config.go | 4 ++ 14 files changed, 126 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fe3da3b..11de5e2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Release Notes. #### Plugins * Support [Pulsar](https://github.com/apache/pulsar-client-go) MQ. * Support [Segmentio-Kafka](https://github.com/segmentio/kafka-go) MQ. +* Support http headers collection for Gin 0.4.0 ------------------ diff --git a/docs/en/agent/plugin-configurations.md b/docs/en/agent/plugin-configurations.md index b69a7b9..b4c7318 100644 --- a/docs/en/agent/plugin-configurations.md +++ b/docs/en/agent/plugin-configurations.md @@ -7,3 +7,5 @@ | sql.collect_parameter | SW_AGENT_PLUGIN_CONFIG_SQL_COLLECT_PARAMETER | false | Collect the parameter of the SQL request. | | redis.max_args_bytes | SW_AGENT_PLUGIN_CONFIG_REDIS_MAX_ARGS_BYTES | 1024 | Limit the bytes size of redis args request. | | reporter.discard | SW_AGENT_REPORTER_DISCARD | false | Discard the reporter. | +| gin.collect_request_headers | SW_AGENT_PLUGIN_CONFIG_GIN_COLLECT_REQUEST_HEADERS | | Collect the http header of gin request. | +| gin.header_length_threshold | SW_AGENT_PLUGIN_CONFIG_GIN_HEADER_LENGTH_THRESHOLD | 2048 | Controlling the length limitation of all header values. | \ No newline at end of file diff --git a/plugins/core/operator/tools.go b/plugins/core/operator/tools.go index f12885f..6a4a804 100644 --- a/plugins/core/operator/tools.go +++ b/plugins/core/operator/tools.go @@ -23,6 +23,7 @@ type ToolsOperator interface { ParseFloat(val string, bitSize int) (float64, error) ParseBool(val string) bool ParseInt(val string, base, bitSize int) (int64, error) + ParseStringArray(val string) ([]string, error) Atoi(val string) (int, error) NewSyncMap() interface{} } diff --git a/plugins/core/tools/strconv.go b/plugins/core/tools/strconv.go index 13210a7..02f030f 100644 --- a/plugins/core/tools/strconv.go +++ b/plugins/core/tools/strconv.go @@ -52,6 +52,17 @@ func ParseInt(val string, base, bitSize int) (int64, error) { return op.Tools().(operator.ToolsOperator).ParseInt(val, base, bitSize) } +func ParseStringArray(val string) ([]string, error) { + if val == "" { + return []string{}, nil + } + op := operator.GetOperator() + if op == nil { + return []string{}, nil + } + return op.Tools().(operator.ToolsOperator).ParseStringArray(val) +} + func Atoi(s string) (int, error) { if s == "" { return 0, nil diff --git a/plugins/core/tracer_tools.go b/plugins/core/tracer_tools.go index 7d43781..aa61050 100644 --- a/plugins/core/tracer_tools.go +++ b/plugins/core/tracer_tools.go @@ -100,6 +100,11 @@ func (t *TracerTools) ParseInt(val string, base, bitSize int) (int64, error) { return strconv.ParseInt(val, base, bitSize) } +func (t *TracerTools) ParseStringArray(val string) ([]string, error) { + newVal := strings.ReplaceAll(val, " ", "") + return strings.Split(newVal, ","), nil +} + func (t *TracerTools) Atoi(val string) (int, error) { return strconv.Atoi(val) } diff --git a/plugins/core/tracing/span.go b/plugins/core/tracing/span.go index edc8005..d21c968 100644 --- a/plugins/core/tracing/span.go +++ b/plugins/core/tracing/span.go @@ -53,6 +53,7 @@ const ( TagStatusCode = "status_code" TagHTTPMethod = "http.method" TagHTTPParams = "http.params" + TagHTTPHeaders = "http.headers" TagDBType = "db.type" TagDBInstance = "db.instance" TagDBStatement = "db.statement" diff --git a/plugins/core/operator/tools.go b/plugins/gin/config.go similarity index 70% copy from plugins/core/operator/tools.go copy to plugins/gin/config.go index f12885f..711aad3 100644 --- a/plugins/core/operator/tools.go +++ b/plugins/gin/config.go @@ -15,14 +15,10 @@ // specific language governing permissions and limitations // under the License. -package operator +package gin -type ToolsOperator interface { - ReflectGetValue(instance interface{}, filters []interface{}) interface{} - GetEnvValue(key string) string - ParseFloat(val string, bitSize int) (float64, error) - ParseBool(val string) bool - ParseInt(val string, base, bitSize int) (int64, error) - Atoi(val string) (int, error) - NewSyncMap() interface{} +//skywalking:config gin +var config struct { + CollectRequestHeaders []string `config:"collect_request_headers"` + HeaderLengthThreshold int `config:"header_length_threshold"` } diff --git a/plugins/gin/intercepter.go b/plugins/gin/intercepter.go index af07621..c1a74a0 100644 --- a/plugins/gin/intercepter.go +++ b/plugins/gin/intercepter.go @@ -19,11 +19,13 @@ package gin import ( "fmt" - - "github.com/gin-gonic/gin" + "net/http" + "strings" "github.com/apache/skywalking-go/plugins/core/operator" "github.com/apache/skywalking-go/plugins/core/tracing" + + "github.com/gin-gonic/gin" ) type ContextInterceptor struct { @@ -45,6 +47,11 @@ func (h *ContextInterceptor) BeforeInvoke(invocation operator.Invocation) error if err != nil { return err } + + if len(config.CollectRequestHeaders) > 0 { + collectRequestHeaders(s, context.Request.Header) + } + invocation.SetContext(s) return nil } @@ -70,3 +77,25 @@ func isFirstHandle(c interface{}) bool { } return true } + +func collectRequestHeaders(span tracing.Span, requestHeaders http.Header) { + var headerTagValues []string + for _, header := range config.CollectRequestHeaders { + var headerValue = requestHeaders.Get(header) + if headerValue != "" { + headerTagValues = append(headerTagValues, header+"="+headerValue) + } + } + + if len(headerTagValues) == 0 { + return + } + + tagValue := strings.Join(headerTagValues, "\n") + if len(tagValue) > config.HeaderLengthThreshold { + maxLen := config.HeaderLengthThreshold + tagValue = tagValue[:maxLen] + } + + span.Tag(tracing.TagHTTPHeaders, tagValue) +} diff --git a/plugins/gin/intercepter_test.go b/plugins/gin/intercepter_test.go index f50dd11..d5ba898 100644 --- a/plugins/gin/intercepter_test.go +++ b/plugins/gin/intercepter_test.go @@ -26,9 +26,9 @@ import ( "github.com/apache/skywalking-go/plugins/core" "github.com/apache/skywalking-go/plugins/core/operator" + "github.com/apache/skywalking-go/plugins/core/tracing" "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" ) @@ -75,3 +75,45 @@ type testWriter struct { func (i *testWriter) Status() int { return 200 } + +func TestCollectHeaders(t *testing.T) { + defer core.ResetTracingContext() + + config.CollectRequestHeaders = []string{"h1", "h2"} + config.HeaderLengthThreshold = 17 + + interceptor := &ContextInterceptor{} + request, err := http.NewRequest("GET", "http://localhost/skywalking/trace", http.NoBody) + assert.Nil(t, err, "new request error should be nil") + request.Header.Set("h1", "h1-value") + request.Header.Set("h2", "h2-value") + + c := &gin.Context{ + Request: request, + Writer: &testWriter{}, + } + + invocation := operator.NewInvocation(c) + err = interceptor.BeforeInvoke(invocation) + assert.Nil(t, err, "before invoke error should be nil") + assert.NotNil(t, invocation.GetContext(), "context should not be nil") + + time.Sleep(100 * time.Millisecond) + + err = interceptor.AfterInvoke(invocation) + assert.Nil(t, err, "after invoke error should be nil") + + time.Sleep(100 * time.Millisecond) + spans := core.GetReportedSpans() + assert.Equal(t, 1, len(spans), "spans length should be 1") + assert.Equal(t, 4, len(spans[0].Tags()), "tags length should be 4") + + index := 0 + for ; index < len(spans[0].Tags()); index++ { + if spans[0].Tags()[index].Key == tracing.TagHTTPHeaders { + break + } + } + assert.Less(t, index, 4, "the index should be less than 4") + assert.Equal(t, "h1=h1-value\nh2=h2", spans[0].Tags()[index].Value, "the tag Value should be h1=h1-value\nh2=h2-value") +} diff --git a/test/plugins/scenarios/gin/bin/startup.sh b/test/plugins/scenarios/gin/bin/startup.sh index 84a29aa..a622e42 100755 --- a/test/plugins/scenarios/gin/bin/startup.sh +++ b/test/plugins/scenarios/gin/bin/startup.sh @@ -19,4 +19,7 @@ home="$(cd "$(dirname $0)"; pwd)" go build ${GO_BUILD_OPTS} -o gin +export SW_AGENT_PLUGIN_CONFIG_GIN_COLLECT_REQUEST_HEADERS=h1,h2 +export SW_AGENT_PLUGIN_CONFIG_GIN_HEADER_LENGTH_THRESHOLD=17 + ./gin \ No newline at end of file diff --git a/test/plugins/scenarios/gin/config/excepted.yml b/test/plugins/scenarios/gin/config/excepted.yml index e928323..d1a3c09 100644 --- a/test/plugins/scenarios/gin/config/excepted.yml +++ b/test/plugins/scenarios/gin/config/excepted.yml @@ -34,6 +34,7 @@ segmentItems: tags: - {key: http.method, value: GET} - {key: url, value: 'localhost:8080/provider'} + - {key: http.headers, value: "h1=h1-value\nh2=h2"} - {key: status_code, value: '200'} refs: - {parentEndpoint: 'GET:/consumer', networkAddress: 'localhost:8080', refType: CrossProcess, diff --git a/test/plugins/scenarios/gin/main.go b/test/plugins/scenarios/gin/main.go index b44133e..8298f99 100644 --- a/test/plugins/scenarios/gin/main.go +++ b/test/plugins/scenarios/gin/main.go @@ -30,13 +30,25 @@ import ( func main() { engine := gin.New() engine.Handle("GET", "/consumer", func(context *gin.Context) { - resp, err := http.Get("http://localhost:8080/provider") + request, err := http.NewRequest("GET", "http://localhost:8080/provider", nil) + if err != nil { + log.Print(err) + context.Status(http.StatusInternalServerError) + return + } + + request.Header.Set("h1", "h1-value") + request.Header.Set("h2", "h2-value") + + client := &http.Client{} + resp, err := client.Do(request) if err != nil { log.Print(err) context.Status(http.StatusInternalServerError) return } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) if err != nil { log.Print(err) diff --git a/tools/go-agent/config/agent.default.yaml b/tools/go-agent/config/agent.default.yaml index ee1e5cd..38c1cdf 100644 --- a/tools/go-agent/config/agent.default.yaml +++ b/tools/go-agent/config/agent.default.yaml @@ -103,3 +103,8 @@ plugin: redis: # Limit the bytes size of redis args request max_args_bytes: ${SW_AGENT_PLUGIN_CONFIG_REDIS_MAX_ARGS_BYTES:1024} + gin: + # Collect the http header of gin request + collect_request_headers: ${SW_AGENT_PLUGIN_CONFIG_GIN_COLLECT_REQUEST_HEADERS:} + # Controlling the length limitation of all header values + header_length_threshold: ${SW_AGENT_PLUGIN_CONFIG_GIN_HEADER_LENGTH_THRESHOLD:2048} \ No newline at end of file diff --git a/tools/go-agent/instrument/plugins/enhance_config.go b/tools/go-agent/instrument/plugins/enhance_config.go index 798fb09..6db9401 100644 --- a/tools/go-agent/instrument/plugins/enhance_config.go +++ b/tools/go-agent/instrument/plugins/enhance_config.go @@ -149,6 +149,8 @@ func NewConfigField(f *dst.Field) (*ConfigField, error) { switch t := f.Type.(type) { case *dst.Ident: conf.Type = t.Name + case *dst.ArrayType: + conf.Type = tools.GenerateTypeNameByExp(t) case *dst.StructType: fs, err := NewConfigFields(t) if err != nil { @@ -235,6 +237,8 @@ func (f *ConfigField) GenerateAssignFieldValue(varName string, field, path []str parseResStr = "if v, err := tools.ParseFloat(result, 64); err != nil { panic(" + parseErrorMessage + ") } else { return v }" case "float": parseResStr = "if v, err := tools.ParseFloat(result, 64); err != nil { panic(" + parseErrorMessage + ") } else { return v }" + case "[]string": + parseResStr = "if v, err := tools.ParseStringArray(result); err != nil { panic(" + parseErrorMessage + ") } else { return v }" default: panic("unsupported config type " + f.Type) }