Skip to content

Commit

Permalink
add metrics for echo framework
Browse files Browse the repository at this point in the history
  • Loading branch information
martinyonatann committed Jan 19, 2025
1 parent 5bec7e6 commit 3433eb8
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 13 deletions.
2 changes: 2 additions & 0 deletions pkg/inst-api/instrumenter/span_suppressor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkg/inst-api/utils/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/echo/echo_server_setup.go"
50 changes: 50 additions & 0 deletions pkg/rules/echo/echo_data_type.go
Original file line number Diff line number Diff line change
@@ -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)
}
109 changes: 109 additions & 0 deletions pkg/rules/echo/echo_otel_instrumenter.go
Original file line number Diff line number Diff line change
@@ -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())
}
44 changes: 36 additions & 8 deletions pkg/rules/echo/echo_server_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -47,5 +75,5 @@ func afterNewEcho(call api.CallContext, e *echo.Echo) {
return
}

e.Use(otelTraceMiddleware())
e.Use(otelTraceMiddleware(call))
}
4 changes: 2 additions & 2 deletions test/echo/v4.0.0/test_echo_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ package main

import (
"fmt"
"io/ioutil"
"io"
"net/http"
"time"

Expand Down Expand Up @@ -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))
Expand Down
64 changes: 64 additions & 0 deletions test/echo/v4.0.0/test_echo_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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)
},
})
}
6 changes: 3 additions & 3 deletions test/echo/v4.0.0/test_echo_pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ package main

import (
"fmt"
"io/ioutil"
"io"
"net/http"
"time"

Expand Down Expand Up @@ -50,10 +50,10 @@ 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/users/1", "http", "1.1", "tcp", "ipv4", "", "127.0.0.1:8080", 200, 0, 8080)
verifier.VerifyHttpServerAttributes(stubs[0][1], "/users/:id", "GET", "http", "tcp", "ipv4", "", "127.0.0.1:8080", "Go-http-client/1.1", "http", "/users/1", "", "/users/:id", 200)
verifier.VerifyHttpServerAttributes(stubs[0][1], "GET /users/1", "GET", "http", "tcp", "ipv4", "", "127.0.0.1:8080", "Go-http-client/1.1", "http", "/users/1", "", "/users/1", 200)
}, 1)
}
7 changes: 7 additions & 0 deletions test/echo_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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"}),
Expand All @@ -47,3 +48,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...)
}

0 comments on commit 3433eb8

Please sign in to comment.