From b2317ca6675c5978576c7a71e6397cab6e67b8ce Mon Sep 17 00:00:00 2001 From: martinpasaribu Date: Sun, 19 Jan 2025 06:21:10 +0700 Subject: [PATCH] add metrics for echo framework --- pkg/inst-api/instrumenter/span_suppressor.go | 2 + pkg/inst-api/utils/scope.go | 1 + pkg/rules/echo/echo_data_type.go | 50 +++++++++ pkg/rules/echo/echo_otel_instrumenter.go | 109 +++++++++++++++++++ pkg/rules/echo/echo_server_setup.go | 44 ++++++-- test/echo/v4.0.0/test_echo_basic.go | 4 +- test/echo/v4.0.0/test_echo_metrics.go | 74 +++++++++++++ test/echo_tests.go | 9 ++ 8 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 pkg/rules/echo/echo_data_type.go create mode 100644 pkg/rules/echo/echo_otel_instrumenter.go create mode 100644 test/echo/v4.0.0/test_echo_metrics.go diff --git a/pkg/inst-api/instrumenter/span_suppressor.go b/pkg/inst-api/instrumenter/span_suppressor.go index 7e9e6a4b..36c18af2 100644 --- a/pkg/inst-api/instrumenter/span_suppressor.go +++ b/pkg/inst-api/instrumenter/span_suppressor.go @@ -34,6 +34,7 @@ var scopeKey = map[string]attribute.Key{ utils.HERTZ_HTTP_SERVER_SCOPE_NAME: utils.HTTP_SERVER_KEY, utils.FIBER_V2_SERVER_SCOPE_NAME: utils.HTTP_SERVER_KEY, utils.ELASTICSEARCH_SCOPE_NAME: utils.HTTP_CLIENT_KEY, + utils.ECHO_SERVER_SCOPE_NAME: utils.HTTP_SERVER_KEY, // grpc utils.GRPC_CLIENT_SCOPE_NAME: utils.RPC_CLIENT_KEY, @@ -58,6 +59,7 @@ var kindKey = map[string]trace.SpanKind{ utils.HERTZ_HTTP_SERVER_SCOPE_NAME: trace.SpanKindServer, utils.FIBER_V2_SERVER_SCOPE_NAME: trace.SpanKindServer, utils.ELASTICSEARCH_SCOPE_NAME: trace.SpanKindClient, + utils.ECHO_SERVER_SCOPE_NAME: trace.SpanKindServer, // grpc utils.GRPC_CLIENT_SCOPE_NAME: trace.SpanKindClient, diff --git a/pkg/inst-api/utils/scope.go b/pkg/inst-api/utils/scope.go index 23203063..994aad9b 100644 --- a/pkg/inst-api/utils/scope.go +++ b/pkg/inst-api/utils/scope.go @@ -36,3 +36,4 @@ const REDIGO_SCOPE_NAME = "pkg/rules/redigo/redigo_client_setup.go" const ELASTICSEARCH_SCOPE_NAME = "pkg/rules/elasticsearch/es_client_setup.go" const GOMICRO_CLIENT_SCOPE_NAME = "pkg/rules/gomicro/client/gomicro_client_setup.go" const GOMICRO_SERVER_SCOPE_NAME = "pkg/rules/gomicro/server/gomicro_server_setup.go" +const ECHO_SERVER_SCOPE_NAME = "pkg/rules/gomicro/server/echo_server_setup.go" diff --git a/pkg/rules/echo/echo_data_type.go b/pkg/rules/echo/echo_data_type.go new file mode 100644 index 00000000..a622b677 --- /dev/null +++ b/pkg/rules/echo/echo_data_type.go @@ -0,0 +1,50 @@ +// Copyright (c) 2024 Alibaba Group Holding Ltd. +// +// 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. + +package echo + +import ( + "net/http" + "net/url" + "strconv" +) + +type echoRequest struct { + method string + url *url.URL + host string + isTls bool + header http.Header + version string +} + +type echoResponse struct { + status int + header http.Header +} + +func getProtocolVersion(majorVersion, minorVersion int) string { + if majorVersion == 1 && minorVersion == 0 { + return "1.0" + } else if majorVersion == 1 && minorVersion == 1 { + return "1.1" + } else if majorVersion == 1 && minorVersion == 2 { + return "1.2" + } else if majorVersion == 2 { + return "2" + } else if majorVersion == 3 { + return "3" + } + return strconv.Itoa(majorVersion) + "." + strconv.Itoa(minorVersion) +} diff --git a/pkg/rules/echo/echo_otel_instrumenter.go b/pkg/rules/echo/echo_otel_instrumenter.go new file mode 100644 index 00000000..10c82e83 --- /dev/null +++ b/pkg/rules/echo/echo_otel_instrumenter.go @@ -0,0 +1,109 @@ +// Copyright (c) 2024 Alibaba Group Holding Ltd. +// +// 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. + +package echo + +import ( + nethttp "net/http" + "strconv" + + "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/inst-api/utils" + "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/inst-api/version" + "go.opentelemetry.io/otel/sdk/instrumentation" + + "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/inst-api-semconv/instrumenter/http" + "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/inst-api-semconv/instrumenter/net" + "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/inst-api/instrumenter" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" +) + +type echoServerAttrsGetter struct{} + +func (e echoServerAttrsGetter) GetRequestMethod(request *echoRequest) string { + return request.method +} +func (e echoServerAttrsGetter) GetHttpRequestHeader(request *echoRequest, name string) []string { + return request.header.Values(name) +} +func (e echoServerAttrsGetter) GetHttpResponseStatusCode(request *echoRequest, response *echoResponse, err error) int { + return response.status +} +func (e echoServerAttrsGetter) GetHttpResponseHeader(request *echoRequest, response *echoResponse, name string) []string { + return response.header.Values(name) +} +func (e echoServerAttrsGetter) GetErrorType(request *echoRequest, response *echoResponse, err error) string { + return nethttp.StatusText(response.status) +} +func (e echoServerAttrsGetter) GetUrlScheme(request *echoRequest) string { + return request.url.Scheme +} +func (e echoServerAttrsGetter) GetUrlPath(request *echoRequest) string { + return request.url.Path +} +func (e echoServerAttrsGetter) GetUrlQuery(request *echoRequest) string { + return request.url.RawQuery +} +func (e echoServerAttrsGetter) GetNetworkType(request *echoRequest, response *echoResponse) string { + return "ipv4" +} +func (e echoServerAttrsGetter) GetNetworkTransport(request *echoRequest, response *echoResponse) string { + return "tcp" +} +func (e echoServerAttrsGetter) GetNetworkProtocolName(request *echoRequest, response *echoResponse) string { + if !request.isTls { + return "http" + } + return "https" +} +func (e echoServerAttrsGetter) GetNetworkProtocolVersion(request *echoRequest, response *echoResponse) string { + return "" +} +func (e echoServerAttrsGetter) GetNetworkLocalInetAddress(request *echoRequest, response *echoResponse) string { + return "" +} +func (e echoServerAttrsGetter) GetNetworkLocalPort(request *echoRequest, response *echoResponse) int { + return 0 +} +func (e echoServerAttrsGetter) GetNetworkPeerInetAddress(request *echoRequest, response *echoResponse) string { + return request.url.Host +} +func (e echoServerAttrsGetter) GetNetworkPeerPort(request *echoRequest, response *echoResponse) int { + port, err := strconv.Atoi(request.url.Port()) + if err != nil { + return 0 + } + return port +} +func (e echoServerAttrsGetter) GetHttpRoute(request *echoRequest) string { + return request.url.Path +} + +func BuildEchoServerOtelInstrumenter() *instrumenter.PropagatingFromUpstreamInstrumenter[*echoRequest, *echoResponse] { + builder := instrumenter.Builder[*echoRequest, *echoResponse]{} + serverGetter := echoServerAttrsGetter{} + commonExtractor := http.HttpCommonAttrsExtractor[*echoRequest, *echoResponse, echoServerAttrsGetter, echoServerAttrsGetter]{HttpGetter: serverGetter, NetGetter: serverGetter} + networkExtractor := net.NetworkAttrsExtractor[*echoRequest, *echoResponse, echoServerAttrsGetter]{Getter: serverGetter} + urlExtractor := net.UrlAttrsExtractor[*echoRequest, *echoResponse, echoServerAttrsGetter]{Getter: serverGetter} + return builder.Init().SetSpanStatusExtractor(http.HttpServerSpanStatusExtractor[*echoRequest, *echoResponse]{Getter: serverGetter}).SetSpanNameExtractor(&http.HttpServerSpanNameExtractor[*echoRequest, *echoResponse]{Getter: serverGetter}). + AddOperationListeners(http.HttpClientMetrics("echo.server")). + SetSpanKindExtractor(&instrumenter.AlwaysServerExtractor[*echoRequest]{}). + SetInstrumentationScope(instrumentation.Scope{ + Name: utils.ECHO_SERVER_SCOPE_NAME, + Version: version.Tag, + }). + AddAttributesExtractor(&http.HttpServerAttrsExtractor[*echoRequest, *echoResponse, echoServerAttrsGetter, echoServerAttrsGetter, echoServerAttrsGetter]{Base: commonExtractor, NetworkExtractor: networkExtractor, UrlExtractor: urlExtractor}).BuildPropagatingFromUpstreamInstrumenter(func(n *echoRequest) propagation.TextMapCarrier { + return propagation.HeaderCarrier(n.header) + }, otel.GetTextMapPropagator()) +} diff --git a/pkg/rules/echo/echo_server_setup.go b/pkg/rules/echo/echo_server_setup.go index 96f02bd4..994b9006 100644 --- a/pkg/rules/echo/echo_server_setup.go +++ b/pkg/rules/echo/echo_server_setup.go @@ -19,21 +19,49 @@ import ( "github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api" echo "github.com/labstack/echo/v4" - "go.opentelemetry.io/otel/sdk/trace" ) -var echoEnabler = instrumenter.NewDefaultInstrumentEnabler() +var ( + echoEnabler = instrumenter.NewDefaultInstrumentEnabler() + echoServerInstrumenter = BuildEchoServerOtelInstrumenter() +) -func otelTraceMiddleware() echo.MiddlewareFunc { +func otelTraceMiddleware(call api.CallContext) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { - if err = next(c); err != nil { + echoRequest := &echoRequest{ + method: c.Request().Method, + url: c.Request().URL, + host: c.Request().Host, + isTls: c.Request().TLS != nil, + header: c.Request().Header, + version: getProtocolVersion(c.Request().ProtoMajor, c.Request().ProtoMinor), + } + + ctx := echoServerInstrumenter.Start(c.Request().Context(), echoRequest) + call.SetParam(1, c.Request().Context()) + data := make(map[string]interface{}, 1) + data["ctx"] = ctx + call.SetData(data) + + err = next(c) + if err != nil { c.Error(err) } - lcs := trace.LocalRootSpanFromGLS() - if lcs != nil && c.Path() != "" && c.Request() != nil && c.Request().URL != nil && (c.Request().URL.Path != c.Path()) { - lcs.SetName(c.Path()) + + requestData, ok := call.GetData().(map[string]interface{}) + if !ok || requestData == nil || requestData["ctx"] == nil { + return } + + echoResponse := new(echoResponse) + if c.Response() != nil { + echoResponse.status = c.Response().Status + echoResponse.header = c.Response().Header() + } + + echoServerInstrumenter.End(ctx, echoRequest, echoResponse, err) + return } } @@ -47,5 +75,5 @@ func afterNewEcho(call api.CallContext, e *echo.Echo) { return } - e.Use(otelTraceMiddleware()) + e.Use(otelTraceMiddleware(call)) } diff --git a/test/echo/v4.0.0/test_echo_basic.go b/test/echo/v4.0.0/test_echo_basic.go index 98a3b34e..2d85ecd1 100644 --- a/test/echo/v4.0.0/test_echo_basic.go +++ b/test/echo/v4.0.0/test_echo_basic.go @@ -16,7 +16,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "net/http" "time" @@ -49,7 +49,7 @@ func main() { if err != nil { panic(err) } - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) fmt.Println(string(body)) verifier.WaitAndAssertTraces(func(stubs []tracetest.SpanStubs) { verifier.VerifyHttpClientAttributes(stubs[0][0], "GET", "GET", "http://127.0.0.1:8080/test", "http", "1.1", "tcp", "ipv4", "", "127.0.0.1:8080", 200, 0, int64(8080)) diff --git a/test/echo/v4.0.0/test_echo_metrics.go b/test/echo/v4.0.0/test_echo_metrics.go new file mode 100644 index 00000000..e5ed6cff --- /dev/null +++ b/test/echo/v4.0.0/test_echo_metrics.go @@ -0,0 +1,74 @@ +// Copyright (c) 2024 Alibaba Group Holding Ltd. +// +// 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. + +package main + +import ( + "net/http" + "strconv" + "time" + + "github.com/alibaba/opentelemetry-go-auto-instrumentation/test/verifier" + echo "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +func setup() { + engine := echo.New() + engine.Use(middleware.Logger()) + engine.GET("/test", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{ + "code": 1, + "msg": c.Path(), + }) + }) + + // Start server + engine.Logger.Fatal(engine.Start(":8080")) +} + +func main() { + go setup() + time.Sleep(5 * time.Second) + client := &http.Client{} + resp, err := client.Get("http://127.0.0.1:8080/test") + defer resp.Body.Close() + if err != nil { + panic(err) + } + time.Sleep(3 * time.Second) + verifier.WaitAndAssertMetrics(map[string]func(metricdata.ResourceMetrics){ + "http.server.request.duration": func(mrs metricdata.ResourceMetrics) { + if len(mrs.ScopeMetrics) <= 0 { + panic("No http.server.request.duration metrics received!") + } + point := mrs.ScopeMetrics[0].Metrics[0].Data.(metricdata.Histogram[float64]) + if point.DataPoints[0].Count <= 0 { + panic("http.server.request.duration metrics count is not positive, actually " + strconv.Itoa(int(point.DataPoints[0].Count))) + } + verifier.VerifyHttpServerMetricsAttributes(point.DataPoints[0].Attributes.ToSlice(), "GET", "/test", "", "http", "1.1", "http", 200) + }, + "http.client.request.duration": func(mrs metricdata.ResourceMetrics) { + if len(mrs.ScopeMetrics) <= 0 { + panic("No http.client.request.duration metrics received!") + } + point := mrs.ScopeMetrics[0].Metrics[0].Data.(metricdata.Histogram[float64]) + if point.DataPoints[0].Count <= 0 { + panic("http.client.request.duration metrics count is not positive, actually " + strconv.Itoa(int(point.DataPoints[0].Count))) + } + verifier.VerifyHttpClientMetricsAttributes(point.DataPoints[0].Attributes.ToSlice(), "GET", "127.0.0.1:8080", "", "http", "1.1", 8080, 200) + }, + }) +} diff --git a/test/echo_tests.go b/test/echo_tests.go index 6e87bc90..7a3628ca 100644 --- a/test/echo_tests.go +++ b/test/echo_tests.go @@ -22,11 +22,14 @@ const echo_module_name = "echo" func init() { TestCases = append(TestCases, NewGeneralTestCase("echo-basic-test", echo_module_name, "v4.0.0", "", "1.18", "", TestBasicEcho), + NewGeneralTestCase("echo-metrics-test", echo_module_name, "v4.0.0", "", "1.18", "", TestMetricsEcho), NewGeneralTestCase("echo-middleware-test", echo_module_name, "v4.0.0", "", "1.18", "", TestEchoMiddleware), NewGeneralTestCase("echo-pattern-test", echo_module_name, "v4.0.0", "", "1.18", "", TestEchoPattern), NewMuzzleTestCase("echo-muzzle-test", echo_dependency_name, echo_module_name, "v4.0.0", "v4.9.1", "1.18", "", []string{"go", "build", "test_echo_basic.go"}), NewMuzzleTestCase("echo-muzzle-test", echo_dependency_name, echo_module_name, "v4.10.0", "", "1.18", "", []string{"go", "build", "test_echo_middleware.go"}), + NewMuzzleTestCase("echo-muzzle-test", echo_dependency_name, echo_module_name, "v4.10.0", "v4.9.1", "1.18", "", []string{"go", "build", "test_echo_metrics.go"}), NewLatestDepthTestCase("echo-latestdepth-test", echo_dependency_name, echo_module_name, "v4.10.0", "", "1.18", "", TestBasicEcho), + NewLatestDepthTestCase("echo-latestdepth-test", echo_dependency_name, echo_module_name, "v4.10.0", "", "1.18", "", TestMetricsEcho), ) } @@ -47,3 +50,9 @@ func TestEchoMiddleware(t *testing.T, env ...string) { RunGoBuild(t, "go", "build", "test_echo_middleware.go") RunApp(t, "test_echo_middleware", env...) } + +func TestMetricsEcho(t *testing.T, env ...string) { + UseApp("echo/v4.0.0") + RunGoBuild(t, "go", "build", "test_echo_metrics.go") + RunApp(t, "test_echo_metrics", env...) +}