diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9b7368d..21f6208 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,9 +27,9 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. -**Kitex version:** +**Version:** -Please provide the version of Kitex you are using. +Please provide the version of {cwgo-pkg} you are using. **Environment:** diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 8cb7338..eb87dc1 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -4,26 +4,24 @@ on: [ pull_request ] jobs: compliant: - runs-on: [ self-hosted, X64 ] + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Check License Header - uses: apache/skywalking-eyes/header@main + uses: apache/skywalking-eyes/header@v0.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Check Spell - uses: crate-ci/typos@master staticcheck: - runs-on: [ self-hosted, X64 ] + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version: "1.21" - uses: actions/cache@v3 with: @@ -45,13 +43,13 @@ jobs: staticcheck_flags: -checks=inherit,-SA1029 lint: - runs-on: [ self-hosted, X64 ] + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version: "1.22" - name: Golangci Lint # https://golangci-lint.run/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c74d2d..2a3cc35 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,29 +4,14 @@ on: [ push, pull_request ] jobs: unit-benchmark-test: - strategy: - matrix: - go: [ 1.17, 1.18, 1.19 ] - os: [ X64, ARM64 ] - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go }} - - # block scenario, comment temporarily -# - uses: actions/cache@v3 -# with: -# path: ~/go/pkg/mod -# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} -# restore-keys: | -# ${{ runner.os }}-go- + go-version: "1.21" - name: Unit Test - run: go test -race -covermode=atomic -coverprofile=coverage.out ./... + run: make test - - name: Benchmark - run: go test -bench=. -benchmem -run=none ./... diff --git a/.golangci.yaml b/.golangci.yaml index c7358bb..d967796 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,14 +2,10 @@ run: # include `vendor` `third_party` `testdata` `examples` `Godeps` `builtin` skip-dirs-use-default: true - skip-dirs: - - kitex_gen - skip-files: - - ".*\\.mock\\.go$" # output configuration options output: # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions - format: colored-line-number + formats: colored-line-number # All available settings of specific linters. # Refer to https://golangci-lint.run/usage/linters linters-settings: @@ -25,12 +21,14 @@ linters-settings: linters: enable: - gofumpt + - goimports - gofmt disable: - errcheck - typecheck - - deadcode - - varcheck - staticcheck issues: exclude-use-default: true + exclude-files: + - ".*\\.mock\\.go$" + exclude-dirs: diff --git a/.licenserc.yaml b/.licenserc.yaml index 6046e12..673db33 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -3,8 +3,11 @@ header: spdx-id: Apache-2.0 copyright-owner: CloudWeGo Authors + paths: - '**/*.go' - '**/*.s' + paths-ignore: + - example/prom/promWithkitex/kitex_gen/** comment: on-failure \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e60a59e..db3d580 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,17 @@ # How to Contribute ## Your First Pull Request -We use github for our codebase. You can start by reading [How To Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). +We use GitHub for our codebase. You can start by reading [How To Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). ## Branch Organization We use [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) as our branch organization, as known as [FDD](https://en.wikipedia.org/wiki/Feature-driven_development) ## Bugs ### 1. How to Find Known Issues -We are using [Github Issues](https://github.com/cloudwego/kitex/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist. +We are using [Github Issues](https://github.com/cloudwego/{project_name}/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist. ### 2. Reporting New Issues -Providing a reduced test code is a recommended way for reporting issues. Then can placed in: +Providing a reduced test code is a recommended way for reporting issues. Then can place in: - Just in issues - [Golang Playground](https://play.golang.org/) @@ -23,9 +23,9 @@ Please do not report the safe disclosure of bugs to public issues. Contact us by ## Submit a Pull Request Before you submit your Pull Request (PR) consider the following guidelines: -1. Search [GitHub](https://github.com/cloudwego/kitex/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate existing efforts. +1. Search [GitHub](https://github.com/cloudwego/{project_name}/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate existing efforts. 2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. Discussing the design upfront helps to ensure that we're ready to accept your work. -3. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the cloudwego/kitex repo. +3. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the cloudwego {project_name} repo. 4. In your forked repository, make your changes in a new git branch: ``` git checkout -b my-fix-branch develop @@ -38,18 +38,14 @@ Before you submit your Pull Request (PR) consider the following guidelines: ``` git push origin my-fix-branch ``` -9. In GitHub, send a pull request to `kitex:develop` +9. In GitHub, send a pull request to `{project_name}:develop` ## Contribution Prerequisites - Our development environment keeps up with [Go Official](https://golang.org/project/). - You need fully checking with lint tools before submit your pull request. [gofmt](https://golang.org/pkg/cmd/gofmt/) and [golangci-lint](https://github.com/golangci/golangci-lint) -- You are familiar with [Github](https://github.com) +- You are familiar with [GitHub](https://github.com) - Maybe you need familiar with [Actions](https://github.com/features/actions)(our default workflow tool). ## Code Style Guides -Also see [Pingcap General advice](https://pingcap.github.io/style-guide/general.html). - -Good resources: - [Effective Go](https://golang.org/doc/effective_go) - [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) -- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..890995f --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +TOOLS_SHELL="./hack/tools.sh" + +.PHONY: test +test: + chmod +x ${TOOLS_SHELL} + @${TOOLS_SHELL} test + @echo "go test finished" + + + +.PHONY: vet +vet: + chmod +x ${TOOLS_SHELL} + @${TOOLS_SHELL} vet + @echo "vet check finished" \ No newline at end of file diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..2ad22e8 --- /dev/null +++ b/example/main.go @@ -0,0 +1,20 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +func main() { +} diff --git a/example/otel/otelwithhertz/client/main.go b/example/otel/otelwithhertz/client/main.go new file mode 100644 index 0000000..e1b66c4 --- /dev/null +++ b/example/otel/otelwithhertz/client/main.go @@ -0,0 +1,66 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelhertz" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/otelprovider" + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/common/hlog" + hertzlogrus "github.com/hertz-contrib/obs-opentelemetry/logging/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" +) + +func main() { + hlog.SetLogger(hertzlogrus.NewLogger()) + hlog.SetLevel(hlog.LevelDebug) + + serviceName := "demo-hertz-client" + + p := otelprovider.NewOpenTelemetryProvider( + otelprovider.WithServiceName(serviceName), + // Support setting ExportEndpoint via environment variables: OTEL_EXPORTER_OTLP_ENDPOINT + otelprovider.WithExportEndpoint("localhost:4317"), + otelprovider.WithHttpServer(), + otelprovider.WithInsecure(), + ) + defer p.Shutdown(context.Background()) + + c, _ := client.NewClient() + c.Use(otelhertz.ClientMiddleware()) + + for { + ctx, span := otel.Tracer("github.com/hertz-contrib/obs-opentelemetry"). + Start(context.Background(), "loop") + + _, b, err := c.Get(ctx, nil, "http://0.0.0.0:8888/ping?foo=bar") + if err != nil { + hlog.CtxErrorf(ctx, err.Error()) + } + + span.SetAttributes(attribute.String("msg", string(b))) + + hlog.CtxInfof(ctx, "hertz client %s", string(b)) + span.End() + + <-time.After(time.Second) + } +} diff --git a/example/otel/otelwithhertz/go.mod b/example/otel/otelwithhertz/go.mod new file mode 100644 index 0000000..ec51138 --- /dev/null +++ b/example/otel/otelwithhertz/go.mod @@ -0,0 +1,15 @@ +module otelwithhertz + +go 1.21 + +require ( + github.com/cloudwego/hertz v0.9.3 + github.com/hertz-contrib/obs-opentelemetry/logging/logrus v0.1.1 +) + +require ( + github.com/sirupsen/logrus v1.8.1 // indirect + go.opentelemetry.io/otel v1.4.1 // indirect + go.opentelemetry.io/otel/trace v1.4.1 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/example/otel/otelwithhertz/server/main.go b/example/otel/otelwithhertz/server/main.go new file mode 100644 index 0000000..78139b6 --- /dev/null +++ b/example/otel/otelwithhertz/server/main.go @@ -0,0 +1,58 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "exampleprom/promWithkitex/kitex_gen/api" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelhertz" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/otelprovider" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/protocol/consts" + hertzlogrus "github.com/hertz-contrib/obs-opentelemetry/logging/logrus" +) + +func main() { + hlog.SetLogger(hertzlogrus.NewLogger()) + hlog.SetLevel(hlog.LevelDebug) + + serviceName := "demo-hertz-server" + p := otelprovider.NewOpenTelemetryProvider( + otelprovider.WithServiceName(serviceName), + // Support setting ExportEndpoint via environment variables: OTEL_EXPORTER_OTLP_ENDPOINT + otelprovider.WithExportEndpoint("localhost:4317"), + otelprovider.WithHttpServer(), + otelprovider.WithInsecure(), + ) + defer p.Shutdown(context.Background()) + + tracer, cfg := otelhertz.NewServerOption() + h := server.Default(tracer) + h.Use(otelhertz.ServerMiddleware(cfg)) + + h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { + req := &api.Request{Message: "my request"} + + hlog.CtxDebugf(c, "message received successfully: %s", req.Message) + ctx.JSON(consts.StatusOK, "resp") + }) + + h.Spin() +} diff --git a/example/otel/otelwithkitex/client/main.go b/example/otel/otelwithkitex/client/main.go new file mode 100644 index 0000000..86b8fa5 --- /dev/null +++ b/example/otel/otelwithkitex/client/main.go @@ -0,0 +1,88 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "exampleprom/promWithkitex/kitex_gen/api" + "exampleprom/promWithkitex/kitex_gen/api/echo" + "math/rand" + "os" + "strconv" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelkitex" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/otelprovider" + "github.com/cloudwego/kitex/client" + "github.com/cloudwego/kitex/pkg/klog" + "github.com/cloudwego/kitex/pkg/rpcinfo" + kitexlogrus "github.com/kitex-contrib/obs-opentelemetry/logging/logrus" + "go.opentelemetry.io/otel" +) + +func main() { + klog.SetLogger(kitexlogrus.NewLogger()) + klog.SetLevel(klog.LevelDebug) + + serviceName := "echo-client" + + p := otelprovider.NewOpenTelemetryProvider( + otelprovider.WithServiceName(serviceName), + // Support setting ExportEndpoint via environment variables: OTEL_EXPORTER_OTLP_ENDPOINT + otelprovider.WithExportEndpoint(":4317"), + otelprovider.WithInsecure(), + otelprovider.WithRPCServer(), + ) + defer p.Shutdown(context.Background()) + + demoServerAddr, ok := os.LookupEnv("DEMO_SERVER_ENDPOINT") + if !ok { + demoServerAddr = "0.0.0.0:8181" + } + + c, err := echo.NewClient( + "echo", + client.WithHostPorts(demoServerAddr), + client.WithSuite(otelkitex.NewClientSuite()), + client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), + ) + if err != nil { + klog.Fatal(err) + } + + // Yields a constantly-changing number + rand.New(rand.NewSource(time.Now().UnixNano())) + for { + call(c) + <-time.After(time.Second) + } +} + +func call(c echo.Client) { + ctx, span := otel.Tracer("client").Start(context.Background(), "root") + defer span.End() + + randomInt := rand.Intn(1000) + req := &api.Request{Message: "my request " + strconv.Itoa(randomInt)} + + resp, err := c.Echo(ctx, req) + if err != nil { + klog.CtxErrorf(ctx, "err %v", err) + } + + klog.CtxInfof(ctx, "req:%v, res:%v", req, resp) +} diff --git a/example/otel/otelwithkitex/go.mod b/example/otel/otelwithkitex/go.mod new file mode 100644 index 0000000..52127b4 --- /dev/null +++ b/example/otel/otelwithkitex/go.mod @@ -0,0 +1,3 @@ +module otelwithkitex + +go 1.21 \ No newline at end of file diff --git a/example/otel/otelwithkitex/server/main.go b/example/otel/otelwithkitex/server/main.go new file mode 100644 index 0000000..9b26e19 --- /dev/null +++ b/example/otel/otelwithkitex/server/main.go @@ -0,0 +1,82 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "errors" + "exampleprom/promWithkitex/kitex_gen/api" + "exampleprom/promWithkitex/kitex_gen/api/echo" + "net" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelkitex" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/otelprovider" + "github.com/cloudwego/kitex/pkg/klog" + "github.com/cloudwego/kitex/pkg/rpcinfo" + "github.com/cloudwego/kitex/server" + kitexlogrus "github.com/kitex-contrib/obs-opentelemetry/logging/logrus" +) + +var _ api.Echo = &EchoImpl{} + +type EchoImpl struct{} + +// Echo implements the Echo interface. +func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) { + klog.CtxDebugf(ctx, "echo called: %s", req.GetMessage()) + nowSec := time.Now().Second() + if nowSec%3 == 1 { + klog.CtxErrorf(ctx, "mock error with request message: %s", req.GetMessage()) + return nil, errors.New("mock error") + } + if nowSec%3 == 2 { + klog.CtxErrorf(ctx, "mock panic with request message: %s", req.GetMessage()) + panic("mock panic") + } + return &api.Response{Message: req.Message}, nil +} + +func main() { + klog.SetLogger(kitexlogrus.NewLogger()) + // set level as debug when needed, default level is info + klog.SetLevel(klog.LevelDebug) + + serviceName := "echo" + p := otelprovider.NewOpenTelemetryProvider( + otelprovider.WithServiceName(serviceName), + // Support setting ExportEndpoint via environment variables: OTEL_EXPORTER_OTLP_ENDPOINT + otelprovider.WithExportEndpoint(":4317"), + otelprovider.WithInsecure(), + otelprovider.WithRPCServer(), + ) + defer p.Shutdown(context.Background()) + + addr, err := net.ResolveTCPAddr("tcp", ":8181") + if err != nil { + panic(err) + } + svr := echo.NewServer( + new(EchoImpl), + server.WithServiceAddr(addr), + server.WithSuite(otelkitex.NewServerSuite()), + server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), + ) + if err := svr.Run(); err != nil { + klog.Fatalf("server stopped with error:", err) + } +} diff --git a/example/prom/go.mod b/example/prom/go.mod new file mode 100644 index 0000000..4cab758 --- /dev/null +++ b/example/prom/go.mod @@ -0,0 +1,5 @@ +module exampleprom + +require ( + github.com/kitex-contrib/obs-opentelemetry/logging/logrus v0.0.0-20230530060140-c76e27f58391 +) \ No newline at end of file diff --git a/example/prom/promWithHertz/main.go b/example/prom/promWithHertz/main.go new file mode 100644 index 0000000..f2d4bb0 --- /dev/null +++ b/example/prom/promWithHertz/main.go @@ -0,0 +1,58 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "fmt" + "net/http" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelhertz" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/promprovider" + "github.com/cloudwego/hertz/pkg/app" + + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + registry := prometheus.NewRegistry() + provider := promprovider.NewPromProvider( + promprovider.WithRegistry(registry), + promprovider.WithHttpServer(), + ) + provider.Serve(":9090", "/metrics-demo") + + tracer := otelhertz.NewServerTracer() + h := server.Default(server.WithTracer(tracer), server.WithHostPorts(":39888")) + h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { + hlog.CtxDebugf(c, "message received successfully") + ctx.JSON(consts.StatusOK, "pong") + }) + + promServerResp, err := http.Get("http://localhost:9090/metrics-demo") + if err != nil { + return + } + if promServerResp.StatusCode == http.StatusOK { + fmt.Print("status is 200\n") + } + // ... + h.Spin() +} diff --git a/example/prom/promWithkitex/client/main.go b/example/prom/promWithkitex/client/main.go new file mode 100644 index 0000000..9170dff --- /dev/null +++ b/example/prom/promWithkitex/client/main.go @@ -0,0 +1,87 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "exampleprom/promWithkitex/kitex_gen/api/echo" + "math/rand" + "os" + "strconv" + "time" + + "exampleprom/promWithkitex/kitex_gen/api" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelkitex" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/promprovider" + "github.com/cloudwego/kitex/client" + "github.com/cloudwego/kitex/pkg/klog" + "github.com/cloudwego/kitex/pkg/rpcinfo" + kitexlogrus "github.com/kitex-contrib/obs-opentelemetry/logging/logrus" + "go.opentelemetry.io/otel" +) + +func main() { + klog.SetLogger(kitexlogrus.NewLogger()) + klog.SetLevel(klog.LevelDebug) + + serviceName := "echo_client" + + p := promprovider.NewPromProvider( + promprovider.WithServiceName(serviceName), + // Support setting ExportEndpoint via environment variables: OTEL_EXPORTER_OTLP_ENDPOINT + promprovider.WithRPCServer(), + ) + defer p.Shutdown(context.Background()) + + demoServerAddr, ok := os.LookupEnv("DEMO_SERVER_ENDPOINT") + if !ok { + demoServerAddr = "0.0.0.0:8181" + } + + c, err := echo.NewClient( + "echo", + client.WithHostPorts(demoServerAddr), + client.WithSuite(otelkitex.NewClientSuite()), + client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), + ) + if err != nil { + klog.Fatal(err) + } + + // Yields a constantly-changing number + rand.New(rand.NewSource(time.Now().UnixNano())) + for { + call(c) + <-time.After(time.Second) + } +} + +func call(c echo.Client) { + ctx, span := otel.Tracer("client").Start(context.Background(), "root") + defer span.End() + + randomInt := rand.Intn(1000) + req := &api.Request{Message: "my request " + strconv.Itoa(randomInt)} + + resp, err := c.Echo(ctx, req) + if err != nil { + klog.CtxErrorf(ctx, "err %v", err) + } + + klog.CtxInfof(ctx, "req:%v, res:%v", req, resp) +} diff --git a/example/prom/promWithkitex/kitex_gen/api/echo.go b/example/prom/promWithkitex/kitex_gen/api/echo.go new file mode 100644 index 0000000..fbede7a --- /dev/null +++ b/example/prom/promWithkitex/kitex_gen/api/echo.go @@ -0,0 +1,802 @@ +// Code generated by thriftgo (0.0.25). DO NOT EDIT. + +package api + +import ( + "context" + "fmt" + "strings" + + "github.com/apache/thrift/lib/go/thrift" +) + +type Request struct { + Message string `thrift:"message,1" json:"message"` +} + +func NewRequest() *Request { + return &Request{} +} + +func (p *Request) GetMessage() string { + return p.Message +} + +func (p *Request) SetMessage(val string) { + p.Message = val +} + +var fieldIDToName_Request = map[int16]string{ + 1: "message", +} + +func (p *Request) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else { + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_Request[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *Request) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return err + } else { + p.Message = v + } + return nil +} + +func (p *Request) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Request"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct end error: ", p), err) +} + +func (p *Request) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("message", thrift.STRING, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(p.Message); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 1 end error: ", p), err) +} + +func (p *Request) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("Request(%+v)", *p) +} + +func (p *Request) DeepEqual(ano *Request) bool { + if p == ano { + return true + } else if p == nil || ano == nil { + return false + } + if !p.Field1DeepEqual(ano.Message) { + return false + } + return true +} + +func (p *Request) Field1DeepEqual(src string) bool { + + if strings.Compare(p.Message, src) != 0 { + return false + } + return true +} + +type Response struct { + Message string `thrift:"message,1" json:"message"` +} + +func NewResponse() *Response { + return &Response{} +} + +func (p *Response) GetMessage() string { + return p.Message +} + +func (p *Response) SetMessage(val string) { + p.Message = val +} + +var fieldIDToName_Response = map[int16]string{ + 1: "message", +} + +func (p *Response) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else { + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_Response[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *Response) ReadField1(iprot thrift.TProtocol) error { + if v, err := iprot.ReadString(); err != nil { + return err + } else { + p.Message = v + } + return nil +} + +func (p *Response) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Response"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct end error: ", p), err) +} + +func (p *Response) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("message", thrift.STRING, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(p.Message); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 1 end error: ", p), err) +} + +func (p *Response) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("Response(%+v)", *p) +} + +func (p *Response) DeepEqual(ano *Response) bool { + if p == ano { + return true + } else if p == nil || ano == nil { + return false + } + if !p.Field1DeepEqual(ano.Message) { + return false + } + return true +} + +func (p *Response) Field1DeepEqual(src string) bool { + + if strings.Compare(p.Message, src) != 0 { + return false + } + return true +} + +type Echo interface { + Echo(ctx context.Context, req *Request) (r *Response, err error) +} + +type EchoClient struct { + c thrift.TClient +} + +func NewEchoClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *EchoClient { + return &EchoClient{ + c: thrift.NewTStandardClient(f.GetProtocol(t), f.GetProtocol(t)), + } +} + +func NewEchoClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *EchoClient { + return &EchoClient{ + c: thrift.NewTStandardClient(iprot, oprot), + } +} + +func NewEchoClient(c thrift.TClient) *EchoClient { + return &EchoClient{ + c: c, + } +} + +func (p *EchoClient) Client_() thrift.TClient { + return p.c +} + +func (p *EchoClient) Echo(ctx context.Context, req *Request) (r *Response, err error) { + var _args EchoEchoArgs + _args.Req = req + var _result EchoEchoResult + if err = p.Client_().Call(ctx, "echo", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} + +type EchoProcessor struct { + processorMap map[string]thrift.TProcessorFunction + handler Echo +} + +func (p *EchoProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) { + p.processorMap[key] = processor +} + +func (p *EchoProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) { + processor, ok = p.processorMap[key] + return processor, ok +} + +func (p *EchoProcessor) ProcessorMap() map[string]thrift.TProcessorFunction { + return p.processorMap +} + +func NewEchoProcessor(handler Echo) *EchoProcessor { + self := &EchoProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)} + self.AddToProcessorMap("echo", &echoProcessorEcho{handler: handler}) + return self +} +func (p *EchoProcessor) Process(ctx context.Context, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + name, _, seqId, err := iprot.ReadMessageBegin() + if err != nil { + return false, err + } + if processor, ok := p.GetProcessorFunction(name); ok { + return processor.Process(ctx, seqId, iprot, oprot) + } + iprot.Skip(thrift.STRUCT) + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name) + oprot.WriteMessageBegin(name, thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, x +} + +type echoProcessorEcho struct { + handler Echo +} + +func (p *echoProcessorEcho) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := EchoEchoArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("echo", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := EchoEchoResult{} + var retval *Response + if retval, err2 = p.handler.Echo(ctx, args.Req); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing echo: "+err2.Error()) + oprot.WriteMessageBegin("echo", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("echo", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type EchoEchoArgs struct { + Req *Request `thrift:"req,1" json:"req"` +} + +func NewEchoEchoArgs() *EchoEchoArgs { + return &EchoEchoArgs{} +} + +var EchoEchoArgs_Req_DEFAULT *Request + +func (p *EchoEchoArgs) GetReq() *Request { + if !p.IsSetReq() { + return EchoEchoArgs_Req_DEFAULT + } + return p.Req +} + +func (p *EchoEchoArgs) SetReq(val *Request) { + p.Req = val +} + +var fieldIDToName_EchoEchoArgs = map[int16]string{ + 1: "req", +} + +func (p *EchoEchoArgs) IsSetReq() bool { + return p.Req != nil +} + +func (p *EchoEchoArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else { + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_EchoEchoArgs[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *EchoEchoArgs) ReadField1(iprot thrift.TProtocol) error { + p.Req = NewRequest() + if err := p.Req.Read(iprot); err != nil { + return err + } + return nil +} + +func (p *EchoEchoArgs) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("echo_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct end error: ", p), err) +} + +func (p *EchoEchoArgs) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("req", thrift.STRUCT, 1); err != nil { + goto WriteFieldBeginError + } + if err := p.Req.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 1 end error: ", p), err) +} + +func (p *EchoEchoArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("EchoEchoArgs(%+v)", *p) +} + +func (p *EchoEchoArgs) DeepEqual(ano *EchoEchoArgs) bool { + if p == ano { + return true + } else if p == nil || ano == nil { + return false + } + if !p.Field1DeepEqual(ano.Req) { + return false + } + return true +} + +func (p *EchoEchoArgs) Field1DeepEqual(src *Request) bool { + + if !p.Req.DeepEqual(src) { + return false + } + return true +} + +type EchoEchoResult struct { + Success *Response `thrift:"success,0" json:"success,omitempty"` +} + +func NewEchoEchoResult() *EchoEchoResult { + return &EchoEchoResult{} +} + +var EchoEchoResult_Success_DEFAULT *Response + +func (p *EchoEchoResult) GetSuccess() *Response { + if !p.IsSetSuccess() { + return EchoEchoResult_Success_DEFAULT + } + return p.Success +} + +func (p *EchoEchoResult) SetSuccess(x interface{}) { + p.Success = x.(*Response) +} + +var fieldIDToName_EchoEchoResult = map[int16]string{ + 0: "success", +} + +func (p *EchoEchoResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *EchoEchoResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else { + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_EchoEchoResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *EchoEchoResult) ReadField0(iprot thrift.TProtocol) error { + p.Success = NewResponse() + if err := p.Success.Read(iprot); err != nil { + return err + } + return nil +} + +func (p *EchoEchoResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("echo_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write struct end error: ", p), err) +} + +func (p *EchoEchoResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T streaming_write field 0 end error: ", p), err) +} + +func (p *EchoEchoResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("EchoEchoResult(%+v)", *p) +} + +func (p *EchoEchoResult) DeepEqual(ano *EchoEchoResult) bool { + if p == ano { + return true + } else if p == nil || ano == nil { + return false + } + if !p.Field0DeepEqual(ano.Success) { + return false + } + return true +} + +func (p *EchoEchoResult) Field0DeepEqual(src *Response) bool { + + if !p.Success.DeepEqual(src) { + return false + } + return true +} diff --git a/example/prom/promWithkitex/kitex_gen/api/echo/client.go b/example/prom/promWithkitex/kitex_gen/api/echo/client.go new file mode 100644 index 0000000..ce3a725 --- /dev/null +++ b/example/prom/promWithkitex/kitex_gen/api/echo/client.go @@ -0,0 +1,51 @@ +// Code generated by Kitex v1.4.1. DO NOT EDIT. + +package echo + +import ( + "context" + + "exampleprom/promWithkitex/kitex_gen/api" + + "github.com/cloudwego/kitex/client" + "github.com/cloudwego/kitex/client/callopt" +) + +// Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. +type Client interface { + Echo(ctx context.Context, req *api.Request, callOptions ...callopt.Option) (r *api.Response, err error) +} + +// NewClient creates a client for the service defined in IDL. +func NewClient(psm string, opts ...client.Option) (Client, error) { + var options []client.Option + options = append(options, client.WithDestService(psm)) + + options = append(options, opts...) + + kc, err := client.NewClient(serviceInfo(), options...) + if err != nil { + return nil, err + } + return &kEchoClient{ + kClient: newServiceClient(kc), + }, nil +} + +// MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. +func MustNewClient(psm string, opts ...client.Option) Client { + kc, err := NewClient(psm, opts...) + if err != nil { + panic(err) + } + return kc +} + +type kEchoClient struct { + *kClient +} + +func (p *kEchoClient) Echo(ctx context.Context, req *api.Request, callOptions ...callopt.Option) (r *api.Response, err error) { + ctx = client.NewCtxWithCallOptions(ctx, callOptions) + return p.kClient.Echo(ctx, req) +} diff --git a/example/prom/promWithkitex/kitex_gen/api/echo/echo.go b/example/prom/promWithkitex/kitex_gen/api/echo/echo.go new file mode 100644 index 0000000..2a2e172 --- /dev/null +++ b/example/prom/promWithkitex/kitex_gen/api/echo/echo.go @@ -0,0 +1,77 @@ +// Code generated by Kitex v1.4.1. DO NOT EDIT. + +package echo + +import ( + "context" + + "exampleprom/promWithkitex/kitex_gen/api" + + "github.com/cloudwego/kitex/client" + kitex "github.com/cloudwego/kitex/pkg/serviceinfo" +) + +func serviceInfo() *kitex.ServiceInfo { + return echoServiceInfo +} + +var echoServiceInfo = newServiceInfo() + +func newServiceInfo() *kitex.ServiceInfo { + serviceName := "Echo" + handlerType := (*api.Echo)(nil) + methods := map[string]kitex.MethodInfo{ + "echo": kitex.NewMethodInfo(echoHandler, newEchoEchoArgs, newEchoEchoResult, false), + } + extra := map[string]interface{}{ + "PackageName": "api", + } + svcInfo := &kitex.ServiceInfo{ + ServiceName: serviceName, + HandlerType: handlerType, + Methods: methods, + PayloadCodec: kitex.Thrift, + KiteXGenVersion: "v1.4.1", + Extra: extra, + } + return svcInfo +} + +func echoHandler(ctx context.Context, handler interface{}, arg, result interface{}) error { + realArg := arg.(*api.EchoEchoArgs) + realResult := result.(*api.EchoEchoResult) + success, err := handler.(api.Echo).Echo(ctx, realArg.Req) + if err != nil { + return err + } + realResult.Success = success + return nil +} + +func newEchoEchoArgs() interface{} { + return api.NewEchoEchoArgs() +} + +func newEchoEchoResult() interface{} { + return api.NewEchoEchoResult() +} + +type kClient struct { + c client.Client +} + +func newServiceClient(c client.Client) *kClient { + return &kClient{ + c: c, + } +} + +func (p *kClient) Echo(ctx context.Context, req *api.Request) (r *api.Response, err error) { + var _args api.EchoEchoArgs + _args.Req = req + var _result api.EchoEchoResult + if err = p.c.Call(ctx, "echo", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} diff --git a/example/prom/promWithkitex/kitex_gen/api/echo/invoker.go b/example/prom/promWithkitex/kitex_gen/api/echo/invoker.go new file mode 100644 index 0000000..c4be35e --- /dev/null +++ b/example/prom/promWithkitex/kitex_gen/api/echo/invoker.go @@ -0,0 +1,25 @@ +// Code generated by Kitex v1.4.1. DO NOT EDIT. + +package echo + +import ( + "exampleprom/promWithkitex/kitex_gen/api" + + "github.com/cloudwego/kitex/server" +) + +// NewInvoker creates a server.Invoker with the given handler and options. +func NewInvoker(handler api.Echo, opts ...server.Option) server.Invoker { + var options []server.Option + + options = append(options, opts...) + + s := server.NewInvoker(options...) + if err := s.RegisterService(serviceInfo(), handler); err != nil { + panic(err) + } + if err := s.Init(); err != nil { + panic(err) + } + return s +} diff --git a/example/prom/promWithkitex/kitex_gen/api/echo/server.go b/example/prom/promWithkitex/kitex_gen/api/echo/server.go new file mode 100644 index 0000000..13266df --- /dev/null +++ b/example/prom/promWithkitex/kitex_gen/api/echo/server.go @@ -0,0 +1,22 @@ +// Code generated by Kitex v1.4.1. DO NOT EDIT. + +package echo + +import ( + "exampleprom/promWithkitex/kitex_gen/api" + + "github.com/cloudwego/kitex/server" +) + +// NewServer creates a server.Server with the given handler and options. +func NewServer(handler api.Echo, opts ...server.Option) server.Server { + var options []server.Option + + options = append(options, opts...) + + svr := server.NewServer(options...) + if err := svr.RegisterService(serviceInfo(), handler); err != nil { + panic(err) + } + return svr +} diff --git a/example/prom/promWithkitex/kitex_gen/api/k-echo.go b/example/prom/promWithkitex/kitex_gen/api/k-echo.go new file mode 100644 index 0000000..3d888fb --- /dev/null +++ b/example/prom/promWithkitex/kitex_gen/api/k-echo.go @@ -0,0 +1,549 @@ +// Code generated by KiteX v1.4.1. DO NOT EDIT. + +package api + +import ( + "bytes" + "fmt" + "reflect" + "strings" + + "github.com/apache/thrift/lib/go/thrift" + + "github.com/cloudwego/kitex/pkg/protocol/bthrift" +) + +// KitexUnusedProtection is used to prevent 'imported and not used' error. +var KitexUnusedProtection = struct{}{} + +// unused protection +var ( + _ = fmt.Formatter(nil) + _ = (*bytes.Buffer)(nil) + _ = (*strings.Builder)(nil) + _ = reflect.Type(nil) + _ = thrift.TProtocol(nil) + _ = bthrift.BinaryWriter(nil) +) + +func (p *Request) FastRead(buf []byte) (int, error) { + var err error + var offset int + var l int + var fieldTypeId thrift.TType + var fieldId int16 + _, l, err = bthrift.Binary.ReadStructBegin(buf) + offset += l + if err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, l, err = bthrift.Binary.ReadFieldBegin(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + l, err = p.FastReadField1(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldError + } + } else { + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + default: + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + + l, err = bthrift.Binary.ReadFieldEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldEndError + } + } + l, err = bthrift.Binary.ReadStructEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadStructEndError + } + + return offset, nil +ReadStructBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_Request[fieldId]), err) +SkipFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) +ReadFieldEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *Request) FastReadField1(buf []byte) (int, error) { + offset := 0 + + if v, l, err := bthrift.Binary.ReadString(buf[offset:]); err != nil { + return offset, err + } else { + offset += l + + p.Message = v + + } + return offset, nil +} + +// for compatibility +func (p *Request) FastWrite(buf []byte) int { + return 0 +} + +func (p *Request) FastWriteNocopy(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + offset += bthrift.Binary.WriteStructBegin(buf[offset:], "Request") + if p != nil { + offset += p.fastWriteField1(buf[offset:], binaryWriter) + } + offset += bthrift.Binary.WriteFieldStop(buf[offset:]) + offset += bthrift.Binary.WriteStructEnd(buf[offset:]) + return offset +} + +func (p *Request) BLength() int { + l := 0 + l += bthrift.Binary.StructBeginLength("Request") + if p != nil { + l += p.field1Length() + } + l += bthrift.Binary.FieldStopLength() + l += bthrift.Binary.StructEndLength() + return l +} + +func (p *Request) fastWriteField1(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + offset += bthrift.Binary.WriteFieldBegin(buf[offset:], "message", thrift.STRING, 1) + offset += bthrift.Binary.WriteStringNocopy(buf[offset:], binaryWriter, p.Message) + + offset += bthrift.Binary.WriteFieldEnd(buf[offset:]) + return offset +} + +func (p *Request) field1Length() int { + l := 0 + l += bthrift.Binary.FieldBeginLength("message", thrift.STRING, 1) + l += bthrift.Binary.StringLengthNocopy(p.Message) + + l += bthrift.Binary.FieldEndLength() + return l +} + +func (p *Response) FastRead(buf []byte) (int, error) { + var err error + var offset int + var l int + var fieldTypeId thrift.TType + var fieldId int16 + _, l, err = bthrift.Binary.ReadStructBegin(buf) + offset += l + if err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, l, err = bthrift.Binary.ReadFieldBegin(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + l, err = p.FastReadField1(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldError + } + } else { + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + default: + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + + l, err = bthrift.Binary.ReadFieldEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldEndError + } + } + l, err = bthrift.Binary.ReadStructEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadStructEndError + } + + return offset, nil +ReadStructBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_Response[fieldId]), err) +SkipFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) +ReadFieldEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *Response) FastReadField1(buf []byte) (int, error) { + offset := 0 + + if v, l, err := bthrift.Binary.ReadString(buf[offset:]); err != nil { + return offset, err + } else { + offset += l + + p.Message = v + + } + return offset, nil +} + +// for compatibility +func (p *Response) FastWrite(buf []byte) int { + return 0 +} + +func (p *Response) FastWriteNocopy(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + offset += bthrift.Binary.WriteStructBegin(buf[offset:], "Response") + if p != nil { + offset += p.fastWriteField1(buf[offset:], binaryWriter) + } + offset += bthrift.Binary.WriteFieldStop(buf[offset:]) + offset += bthrift.Binary.WriteStructEnd(buf[offset:]) + return offset +} + +func (p *Response) BLength() int { + l := 0 + l += bthrift.Binary.StructBeginLength("Response") + if p != nil { + l += p.field1Length() + } + l += bthrift.Binary.FieldStopLength() + l += bthrift.Binary.StructEndLength() + return l +} + +func (p *Response) fastWriteField1(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + offset += bthrift.Binary.WriteFieldBegin(buf[offset:], "message", thrift.STRING, 1) + offset += bthrift.Binary.WriteStringNocopy(buf[offset:], binaryWriter, p.Message) + + offset += bthrift.Binary.WriteFieldEnd(buf[offset:]) + return offset +} + +func (p *Response) field1Length() int { + l := 0 + l += bthrift.Binary.FieldBeginLength("message", thrift.STRING, 1) + l += bthrift.Binary.StringLengthNocopy(p.Message) + + l += bthrift.Binary.FieldEndLength() + return l +} + +func (p *EchoEchoArgs) FastRead(buf []byte) (int, error) { + var err error + var offset int + var l int + var fieldTypeId thrift.TType + var fieldId int16 + _, l, err = bthrift.Binary.ReadStructBegin(buf) + offset += l + if err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, l, err = bthrift.Binary.ReadFieldBegin(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.STRUCT { + l, err = p.FastReadField1(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldError + } + } else { + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + default: + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + + l, err = bthrift.Binary.ReadFieldEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldEndError + } + } + l, err = bthrift.Binary.ReadStructEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadStructEndError + } + + return offset, nil +ReadStructBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_EchoEchoArgs[fieldId]), err) +SkipFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) +ReadFieldEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *EchoEchoArgs) FastReadField1(buf []byte) (int, error) { + offset := 0 + p.Req = NewRequest() + if l, err := p.Req.FastRead(buf[offset:]); err != nil { + return offset, err + } else { + offset += l + } + return offset, nil +} + +// for compatibility +func (p *EchoEchoArgs) FastWrite(buf []byte) int { + return 0 +} + +func (p *EchoEchoArgs) FastWriteNocopy(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + offset += bthrift.Binary.WriteStructBegin(buf[offset:], "echo_args") + if p != nil { + offset += p.fastWriteField1(buf[offset:], binaryWriter) + } + offset += bthrift.Binary.WriteFieldStop(buf[offset:]) + offset += bthrift.Binary.WriteStructEnd(buf[offset:]) + return offset +} + +func (p *EchoEchoArgs) BLength() int { + l := 0 + l += bthrift.Binary.StructBeginLength("echo_args") + if p != nil { + l += p.field1Length() + } + l += bthrift.Binary.FieldStopLength() + l += bthrift.Binary.StructEndLength() + return l +} + +func (p *EchoEchoArgs) fastWriteField1(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + offset += bthrift.Binary.WriteFieldBegin(buf[offset:], "req", thrift.STRUCT, 1) + offset += p.Req.FastWriteNocopy(buf[offset:], binaryWriter) + offset += bthrift.Binary.WriteFieldEnd(buf[offset:]) + return offset +} + +func (p *EchoEchoArgs) field1Length() int { + l := 0 + l += bthrift.Binary.FieldBeginLength("req", thrift.STRUCT, 1) + l += p.Req.BLength() + l += bthrift.Binary.FieldEndLength() + return l +} + +func (p *EchoEchoResult) FastRead(buf []byte) (int, error) { + var err error + var offset int + var l int + var fieldTypeId thrift.TType + var fieldId int16 + _, l, err = bthrift.Binary.ReadStructBegin(buf) + offset += l + if err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, l, err = bthrift.Binary.ReadFieldBegin(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + l, err = p.FastReadField0(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldError + } + } else { + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + default: + l, err = bthrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + + l, err = bthrift.Binary.ReadFieldEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldEndError + } + } + l, err = bthrift.Binary.ReadStructEnd(buf[offset:]) + offset += l + if err != nil { + goto ReadStructEndError + } + + return offset, nil +ReadStructBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct begin error: ", p), err) +ReadFieldBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field %d '%s' error: ", p, fieldId, fieldIDToName_EchoEchoResult[fieldId]), err) +SkipFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) +ReadFieldEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read field end error", p), err) +ReadStructEndError: + return offset, thrift.PrependError(fmt.Sprintf("%T streaming_read struct end error: ", p), err) +} + +func (p *EchoEchoResult) FastReadField0(buf []byte) (int, error) { + offset := 0 + p.Success = NewResponse() + if l, err := p.Success.FastRead(buf[offset:]); err != nil { + return offset, err + } else { + offset += l + } + return offset, nil +} + +// for compatibility +func (p *EchoEchoResult) FastWrite(buf []byte) int { + return 0 +} + +func (p *EchoEchoResult) FastWriteNocopy(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + offset += bthrift.Binary.WriteStructBegin(buf[offset:], "echo_result") + if p != nil { + offset += p.fastWriteField0(buf[offset:], binaryWriter) + } + offset += bthrift.Binary.WriteFieldStop(buf[offset:]) + offset += bthrift.Binary.WriteStructEnd(buf[offset:]) + return offset +} + +func (p *EchoEchoResult) BLength() int { + l := 0 + l += bthrift.Binary.StructBeginLength("echo_result") + if p != nil { + l += p.field0Length() + } + l += bthrift.Binary.FieldStopLength() + l += bthrift.Binary.StructEndLength() + return l +} + +func (p *EchoEchoResult) fastWriteField0(buf []byte, binaryWriter bthrift.BinaryWriter) int { + offset := 0 + if p.IsSetSuccess() { + offset += bthrift.Binary.WriteFieldBegin(buf[offset:], "success", thrift.STRUCT, 0) + offset += p.Success.FastWriteNocopy(buf[offset:], binaryWriter) + offset += bthrift.Binary.WriteFieldEnd(buf[offset:]) + } + return offset +} + +func (p *EchoEchoResult) field0Length() int { + l := 0 + if p.IsSetSuccess() { + l += bthrift.Binary.FieldBeginLength("success", thrift.STRUCT, 0) + l += p.Success.BLength() + l += bthrift.Binary.FieldEndLength() + } + return l +} + +func (p *EchoEchoArgs) GetFirstArgument() interface{} { + return p.Req +} + +func (p *EchoEchoResult) GetResult() interface{} { + return p.Success +} diff --git a/example/prom/promWithkitex/server/main.go b/example/prom/promWithkitex/server/main.go new file mode 100644 index 0000000..5f5942a --- /dev/null +++ b/example/prom/promWithkitex/server/main.go @@ -0,0 +1,81 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "errors" + "exampleprom/promWithkitex/kitex_gen/api/echo" + "net" + "time" + + "exampleprom/promWithkitex/kitex_gen/api" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelkitex" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/promprovider" + "github.com/cloudwego/kitex/pkg/klog" + "github.com/cloudwego/kitex/pkg/rpcinfo" + "github.com/cloudwego/kitex/server" + kitexlogrus "github.com/kitex-contrib/obs-opentelemetry/logging/logrus" +) + +var _ api.Echo = &EchoImpl{} + +type EchoImpl struct{} + +// Echo implements the Echo interface. +func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) { + klog.CtxDebugf(ctx, "echo called: %s", req.GetMessage()) + nowSec := time.Now().Second() + if nowSec%3 == 1 { + klog.CtxErrorf(ctx, "mock error with request message: %s", req.GetMessage()) + return nil, errors.New("mock error") + } + if nowSec%3 == 2 { + klog.CtxErrorf(ctx, "mock panic with request message: %s", req.GetMessage()) + panic("mock panic") + } + return &api.Response{Message: req.Message}, nil +} + +func main() { + klog.SetLogger(kitexlogrus.NewLogger()) + // set level as debug when needed, default level is info + klog.SetLevel(klog.LevelDebug) + + serviceName := "echo" + + p := promprovider.NewPromProvider( + promprovider.WithServiceName(serviceName), + promprovider.WithRPCServer(), + ) + defer p.Shutdown(context.Background()) + p.Serve(":9091", "/kitexserver") + addr, err := net.ResolveTCPAddr("tcp", ":8181") + if err != nil { + panic(err) + } + svr := echo.NewServer( + new(EchoImpl), + server.WithServiceAddr(addr), + server.WithSuite(otelkitex.NewServerSuite()), + server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: serviceName}), + ) + if err := svr.Run(); err != nil { + klog.Fatalf("server stopped with error:", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..931b5cc --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/cloudwego-contrib/cwgo-pkg + +go 1.21 + +replace github.com/apache/thrift => github.com/apache/thrift v0.13.0 diff --git a/go.work b/go.work new file mode 100644 index 0000000..cc39333 --- /dev/null +++ b/go.work @@ -0,0 +1,20 @@ +go 1.21 + +use ( + . + ./log/logging/logrus + ./log/logging/slog + ./log/logging/zap + ./log/logging/zerolog + telemetry + ./telemetry/instrumentation/otellogrus + ./telemetry/instrumentation/otelslog + ./telemetry/instrumentation/otelzap + ./telemetry/instrumentation/otelzerolog + example/prom + example/otel/otelwithhertz + example/otel/otelwithkitex + +) + +replace github.com/apache/thrift => github.com/apache/thrift v0.13.0 diff --git a/hack/resolve-modules.sh b/hack/resolve-modules.sh new file mode 100644 index 0000000..acef142 --- /dev/null +++ b/hack/resolve-modules.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# This is used by the linter action. +# Recursively finds all directories with a go.mod file and creates +# a GitHub Actions JSON output option. + +set -o errexit + +HOME=$( + cd "$(dirname "${BASH_SOURCE[0]}")" && + cd .. && + pwd +) + +source "${HOME}/hack/util.sh" +all_modules=$(util::find_modules) +PATHS="" +for mod in $all_modules; do + PATHS+=$(printf '{"workdir":"%s"},' ${mod}) +done + +echo "::set-output name=matrix::{\"include\":[${PATHS%?}]}" \ No newline at end of file diff --git a/hack/tools.sh b/hack/tools.sh new file mode 100644 index 0000000..d11c8b6 --- /dev/null +++ b/hack/tools.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +HOME=$( + cd "$(dirname "${BASH_SOURCE[0]}")" && + cd .. && + pwd +) + +source "${HOME}/hack/util.sh" + +all_modules=$(util::find_modules) + +# test all mod +function test() { + for mod in $all_modules; do + pushd "$mod" >/dev/null && + echo "go test $(sed -n 1p go.mod | cut -d ' ' -f2)" && + go test -race -covermode=atomic -coverprofile=coverage.out ./... + popd >/dev/null || exit + done +} + +# vet all mod +function vet() { + for mod in $all_modules; do + pushd "$mod" >/dev/null && + echo "go vet $(sed -n 1p go.mod | cut -d ' ' -f2)" && + go vet -stdmethods=false ./... + popd >/dev/null || exit + done +} + +function help() { + echo "use: test,vet" +} + +case $1 in +vet) + vet + ;; +test) + test + ;; +*) + help + ;; +esac diff --git a/hack/util.sh b/hack/util.sh new file mode 100644 index 0000000..8954b36 --- /dev/null +++ b/hack/util.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# find all go mod path +# returns an array contains mod path +function util::find_modules() { + find . -mindepth 2 -not \( \ + \( \ + -path './output' \ + -o -path './.git' \ + -o -path '*/third_party/*' \ + -o -path '*/vendor/*' \ + \) -prune \ + \) -name 'go.mod' -print0 | xargs -0 -I {} dirname {} +} \ No newline at end of file diff --git a/log/logging/logrus/go.mod b/log/logging/logrus/go.mod new file mode 100644 index 0000000..5570c44 --- /dev/null +++ b/log/logging/logrus/go.mod @@ -0,0 +1,14 @@ +module github.com/cloudwego-contrib/cwgo-pkg/log/logging/logrus + +go 1.21 + +require ( + github.com/cloudwego/hertz v0.9.2 + github.com/sirupsen/logrus v1.9.2 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/stretchr/testify v1.9.0 // indirect + golang.org/x/sys v0.21.0 // indirect +) diff --git a/log/logging/logrus/go.sum b/log/logging/logrus/go.sum new file mode 100644 index 0000000..5ea7b6b --- /dev/null +++ b/log/logging/logrus/go.sum @@ -0,0 +1,54 @@ +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/hertz v0.9.2 h1:VbqddZ5RuvcgxzfxvXcmTiRisGYoo0+WnHGeDJKhjqI= +github.com/cloudwego/netpoll v0.6.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/log/logging/logrus/logger.go b/log/logging/logrus/logger.go new file mode 100644 index 0000000..e2e3cc5 --- /dev/null +++ b/log/logging/logrus/logger.go @@ -0,0 +1,190 @@ +/* + * Copyright 2022 CloudWeGo 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. + * + * MIT License + * + * Copyright (c) 2019-present Fenny and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE.E SOFTWARE. + * + * This file may have been modified by CloudWeGo authors. All CloudWeGo + * Modifications are Copyright 2022 CloudWeGo Authors. + */ + +package logrus + +import ( + "context" + "io" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/sirupsen/logrus" +) + +var _ hlog.FullLogger = (*Logger)(nil) + +// Logger otellogrus impl +type Logger struct { + l *logrus.Logger +} + +// NewLogger create a logger +func NewLogger(opts ...Option) *Logger { + cfg := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(cfg) + } + + // attach hook + for _, hook := range cfg.hooks { + cfg.logger.AddHook(hook) + } + + return &Logger{ + l: cfg.logger, + } +} + +func (l *Logger) Logger() *logrus.Logger { + return l.l +} + +func (l *Logger) Trace(v ...interface{}) { + l.l.Trace(v...) +} + +func (l *Logger) Debug(v ...interface{}) { + l.l.Debug(v...) +} + +func (l *Logger) Info(v ...interface{}) { + l.l.Info(v...) +} + +func (l *Logger) Notice(v ...interface{}) { + l.l.Warn(v...) +} + +func (l *Logger) Warn(v ...interface{}) { + l.l.Warn(v...) +} + +func (l *Logger) Error(v ...interface{}) { + l.l.Error(v...) +} + +func (l *Logger) Fatal(v ...interface{}) { + l.l.Fatal(v...) +} + +func (l *Logger) Tracef(format string, v ...interface{}) { + l.l.Tracef(format, v...) +} + +func (l *Logger) Debugf(format string, v ...interface{}) { + l.l.Debugf(format, v...) +} + +func (l *Logger) Infof(format string, v ...interface{}) { + l.l.Infof(format, v...) +} + +func (l *Logger) Noticef(format string, v ...interface{}) { + l.l.Warnf(format, v...) +} + +func (l *Logger) Warnf(format string, v ...interface{}) { + l.l.Warnf(format, v...) +} + +func (l *Logger) Errorf(format string, v ...interface{}) { + l.l.Errorf(format, v...) +} + +func (l *Logger) Fatalf(format string, v ...interface{}) { + l.l.Fatalf(format, v...) +} + +func (l *Logger) CtxTracef(ctx context.Context, format string, v ...interface{}) { + l.l.WithContext(ctx).Tracef(format, v...) +} + +func (l *Logger) CtxDebugf(ctx context.Context, format string, v ...interface{}) { + l.l.WithContext(ctx).Debugf(format, v...) +} + +func (l *Logger) CtxInfof(ctx context.Context, format string, v ...interface{}) { + l.l.WithContext(ctx).Infof(format, v...) +} + +func (l *Logger) CtxNoticef(ctx context.Context, format string, v ...interface{}) { + l.l.WithContext(ctx).Warnf(format, v...) +} + +func (l *Logger) CtxWarnf(ctx context.Context, format string, v ...interface{}) { + l.l.WithContext(ctx).Warnf(format, v...) +} + +func (l *Logger) CtxErrorf(ctx context.Context, format string, v ...interface{}) { + l.l.WithContext(ctx).Errorf(format, v...) +} + +func (l *Logger) CtxFatalf(ctx context.Context, format string, v ...interface{}) { + l.l.WithContext(ctx).Fatalf(format, v...) +} + +func (l *Logger) SetLevel(level hlog.Level) { + var lv logrus.Level + switch level { + case hlog.LevelTrace: + lv = logrus.TraceLevel + case hlog.LevelDebug: + lv = logrus.DebugLevel + case hlog.LevelInfo: + lv = logrus.InfoLevel + case hlog.LevelWarn, hlog.LevelNotice: + lv = logrus.WarnLevel + case hlog.LevelError: + lv = logrus.ErrorLevel + case hlog.LevelFatal: + lv = logrus.FatalLevel + default: + lv = logrus.WarnLevel + } + l.l.SetLevel(lv) +} + +func (l *Logger) SetOutput(writer io.Writer) { + l.l.SetOutput(writer) +} diff --git a/log/logging/logrus/logger_test.go b/log/logging/logrus/logger_test.go new file mode 100644 index 0000000..56f777b --- /dev/null +++ b/log/logging/logrus/logger_test.go @@ -0,0 +1,62 @@ +// Copyright 2022 CloudWeGo 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. + +package logrus_test + +import ( + "context" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + cwlogrus "github.com/cloudwego-contrib/cwgo-pkg/log/logging/logrus" + + "github.com/sirupsen/logrus" +) + +func TestLogger(t *testing.T) { + ctx := context.Background() + + logger := cwlogrus.NewLogger(cwlogrus.WithLogger(logrus.New())) + + logger.Logger().Info("log from origin otellogrus") + + hlog.SetLogger(logger) + hlog.SetLevel(hlog.LevelError) + hlog.SetLevel(hlog.LevelWarn) + hlog.SetLevel(hlog.LevelInfo) + hlog.SetLevel(hlog.LevelDebug) + hlog.SetLevel(hlog.LevelTrace) + + hlog.Trace("trace") + hlog.Debug("debug") + hlog.Info("info") + hlog.Notice("notice") + hlog.Warn("warn") + hlog.Error("error") + + hlog.Tracef("log level: %s", "trace") + hlog.Debugf("log level: %s", "debug") + hlog.Infof("log level: %s", "info") + hlog.Noticef("log level: %s", "notice") + hlog.Warnf("log level: %s", "warn") + hlog.Errorf("log level: %s", "error") + + hlog.CtxTracef(ctx, "log level: %s", "trace") + hlog.CtxDebugf(ctx, "log level: %s", "debug") + hlog.CtxInfof(ctx, "log level: %s", "info") + hlog.CtxNoticef(ctx, "log level: %s", "notice") + hlog.CtxWarnf(ctx, "log level: %s", "warn") + hlog.CtxErrorf(ctx, "log level: %s", "error") +} diff --git a/log/logging/logrus/option.go b/log/logging/logrus/option.go new file mode 100644 index 0000000..58bb0a3 --- /dev/null +++ b/log/logging/logrus/option.go @@ -0,0 +1,61 @@ +// Copyright 2022 CloudWeGo 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. + +package logrus + +import ( + "github.com/sirupsen/logrus" +) + +// Option logger options +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + logger *logrus.Logger + hooks []logrus.Hook +} + +func defaultConfig() *config { + // new logger + logger := logrus.New() + // default json format + logger.SetFormatter(new(logrus.JSONFormatter)) + + return &config{ + logger: logger, + hooks: []logrus.Hook{}, + } +} + +// WithLogger configures logger +func WithLogger(logger *logrus.Logger) Option { + return option(func(cfg *config) { + cfg.logger = logger + }) +} + +// WithHook configures otellogrus hook +func WithHook(hook logrus.Hook) Option { + return option(func(cfg *config) { + cfg.hooks = append(cfg.hooks, hook) + }) +} diff --git a/log/logging/slog/go.mod b/log/logging/slog/go.mod new file mode 100644 index 0000000..319b5f8 --- /dev/null +++ b/log/logging/slog/go.mod @@ -0,0 +1,17 @@ +module github.com/cloudwego-contrib/cwgo-pkg/log/logging/slog + +go 1.21 + +require ( + github.com/cloudwego/hertz v0.9.2 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/log/logging/slog/go.sum b/log/logging/slog/go.sum new file mode 100644 index 0000000..1ea2121 --- /dev/null +++ b/log/logging/slog/go.sum @@ -0,0 +1,64 @@ +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/hertz v0.9.2 h1:VbqddZ5RuvcgxzfxvXcmTiRisGYoo0+WnHGeDJKhjqI= +github.com/cloudwego/netpoll v0.6.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/log/logging/slog/logger.go b/log/logging/slog/logger.go new file mode 100644 index 0000000..f7826b1 --- /dev/null +++ b/log/logging/slog/logger.go @@ -0,0 +1,223 @@ +// Copyright 2023 CloudWeGo 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. + +package slog + +import ( + "context" + "fmt" + "io" + "log/slog" + + "github.com/cloudwego/hertz/pkg/common/hlog" +) + +const ( + LevelTrace = slog.Level(-8) + LevelNotice = slog.Level(2) + LevelFatal = slog.Level(12) +) + +var _ hlog.FullLogger = (*Logger)(nil) + +func NewLogger(opts ...Option) *Logger { + config := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(config) + } + + if !config.withLevel && config.withHandlerOptions && config.handlerOptions.Level != nil { + lvl := &slog.LevelVar{} + lvl.Set(config.handlerOptions.Level.Level()) + config.level = lvl + } + config.handlerOptions.Level = config.level + + var replaceAttrDefined bool + if config.handlerOptions.ReplaceAttr == nil { + replaceAttrDefined = false + } else { + replaceAttrDefined = true + } + + replaceFun := config.handlerOptions.ReplaceAttr + + replaceAttr := func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + switch level { + case LevelTrace: + a.Value = slog.StringValue("Trace") + case slog.LevelDebug: + a.Value = slog.StringValue("Debug") + case slog.LevelInfo: + a.Value = slog.StringValue("Info") + case LevelNotice: + a.Value = slog.StringValue("Notice") + case slog.LevelWarn: + a.Value = slog.StringValue("Warn") + case slog.LevelError: + a.Value = slog.StringValue("Error") + case LevelFatal: + a.Value = slog.StringValue("Fatal") + default: + a.Value = slog.StringValue("Warn") + } + } + if replaceAttrDefined { + return replaceFun(groups, a) + } else { + return a + } + } + config.handlerOptions.ReplaceAttr = replaceAttr + + return &Logger{ + l: slog.New(slog.NewJSONHandler(config.output, config.handlerOptions)), + cfg: config, + } +} + +// Logger otelslog impl +type Logger struct { + l *slog.Logger + cfg *config +} + +func (l *Logger) Logger() *slog.Logger { + return l.l +} + +func (l *Logger) log(level hlog.Level, v ...any) { + lvl := tranSLevel(level) + l.l.Log(context.TODO(), lvl, fmt.Sprint(v...)) +} + +func (l *Logger) logf(level hlog.Level, format string, kvs ...any) { + lvl := tranSLevel(level) + l.l.Log(context.TODO(), lvl, fmt.Sprintf(format, kvs...)) +} + +func (l *Logger) ctxLogf(level hlog.Level, ctx context.Context, format string, v ...any) { + lvl := tranSLevel(level) + l.l.Log(ctx, lvl, fmt.Sprintf(format, v...)) +} + +func (l *Logger) Trace(v ...any) { + l.log(hlog.LevelTrace, v...) +} + +func (l *Logger) Debug(v ...any) { + l.log(hlog.LevelDebug, v...) +} + +func (l *Logger) Info(v ...any) { + l.log(hlog.LevelInfo, v...) +} + +func (l *Logger) Notice(v ...any) { + l.log(hlog.LevelNotice, v...) +} + +func (l *Logger) Warn(v ...any) { + l.log(hlog.LevelWarn, v...) +} + +func (l *Logger) Error(v ...any) { + l.log(hlog.LevelError, v...) +} + +func (l *Logger) Fatal(v ...any) { + l.log(hlog.LevelFatal, v...) +} + +func (l *Logger) Tracef(format string, v ...any) { + l.logf(hlog.LevelTrace, format, v...) +} + +func (l *Logger) Debugf(format string, v ...any) { + l.logf(hlog.LevelDebug, format, v...) +} + +func (l *Logger) Infof(format string, v ...any) { + l.logf(hlog.LevelInfo, format, v...) +} + +func (l *Logger) Noticef(format string, v ...any) { + l.logf(hlog.LevelNotice, format, v...) +} + +func (l *Logger) Warnf(format string, v ...any) { + l.logf(hlog.LevelWarn, format, v...) +} + +func (l *Logger) Errorf(format string, v ...any) { + l.logf(hlog.LevelError, format, v...) +} + +func (l *Logger) Fatalf(format string, v ...any) { + l.logf(hlog.LevelFatal, format, v...) +} + +func (l *Logger) CtxTracef(ctx context.Context, format string, v ...any) { + l.ctxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxDebugf(ctx context.Context, format string, v ...any) { + l.ctxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxInfof(ctx context.Context, format string, v ...any) { + l.ctxLogf(hlog.LevelInfo, ctx, format, v...) +} + +func (l *Logger) CtxNoticef(ctx context.Context, format string, v ...any) { + l.ctxLogf(hlog.LevelNotice, ctx, format, v...) +} + +func (l *Logger) CtxWarnf(ctx context.Context, format string, v ...any) { + l.ctxLogf(hlog.LevelWarn, ctx, format, v...) +} + +func (l *Logger) CtxErrorf(ctx context.Context, format string, v ...any) { + l.ctxLogf(hlog.LevelError, ctx, format, v...) +} + +func (l *Logger) CtxFatalf(ctx context.Context, format string, v ...any) { + l.ctxLogf(hlog.LevelFatal, ctx, format, v...) +} + +func (l *Logger) SetLevel(level hlog.Level) { + lvl := tranSLevel(level) + l.cfg.level.Set(lvl) +} + +func (l *Logger) SetOutput(writer io.Writer) { + l.cfg.output = writer + l.l = slog.New(slog.NewJSONHandler(writer, l.cfg.handlerOptions)) +} + +func (l *Logger) SetLogger(log *slog.Logger) { + l.l = log +} + +func (l *Logger) GetHandler() *slog.HandlerOptions { + return l.cfg.handlerOptions +} + +func (l *Logger) GetOutput() io.Writer { + return l.cfg.output +} diff --git a/log/logging/slog/logger_test.go b/log/logging/slog/logger_test.go new file mode 100644 index 0000000..f0aeb92 --- /dev/null +++ b/log/logging/slog/logger_test.go @@ -0,0 +1,198 @@ +// Copyright 2023 CloudWeGo 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. + +package slog + +import ( + "bufio" + "bytes" + "context" + "log/slog" + "os" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/stretchr/testify/assert" +) + +const ( + traceMsg = "this is a trace log" + debugMsg = "this is a debug log" + infoMsg = "this is a info log" + warnMsg = "this is a warn log" + noticeMsg = "this is a notice log" + errorMsg = "this is a error log" + fatalMsg = "this is a fatal log" + logFileName = "otelhertz.log" +) + +// TestLogger test logger work with otelhertz +func TestLogger(t *testing.T) { + buf := new(bytes.Buffer) + logger := NewLogger() + + hlog.SetLogger(logger) + hlog.SetOutput(buf) + hlog.SetLevel(hlog.LevelError) + + hlog.Info(infoMsg) + assert.Equal(t, "", buf.String()) + + hlog.Error(errorMsg) + // test SetLevel + assert.Contains(t, buf.String(), errorMsg) + + buf.Reset() + f, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + t.Error(err) + } + + defer os.Remove(logFileName) + + hlog.SetOutput(f) + + hlog.Info(infoMsg) + hlog.Error(errorMsg) + _ = f.Sync() + + readF, err := os.OpenFile(logFileName, os.O_RDONLY, 0o400) + if err != nil { + t.Error(err) + } + line, _ := bufio.NewReader(readF).ReadString('\n') + + // test SetOutput + assert.Contains(t, line, errorMsg) +} + +func TestWithLevel(t *testing.T) { + buf := new(bytes.Buffer) + lvl := &slog.LevelVar{} + lvl.Set(slog.LevelError) + logger := NewLogger(WithLevel(lvl)) + + hlog.SetLogger(logger) + hlog.SetOutput(buf) + + hlog.Notice(infoMsg) + assert.Equal(t, "", buf.String()) + + hlog.Error(errorMsg) + assert.Contains(t, buf.String(), errorMsg) + + buf.Reset() + hlog.SetLevel(hlog.LevelDebug) + hlog.Debug(debugMsg) + + assert.Contains(t, buf.String(), debugMsg) +} + +func TestWithHandlerOptions(t *testing.T) { + buf := new(bytes.Buffer) + logger := NewLogger(WithHandlerOptions(&slog.HandlerOptions{Level: slog.LevelError, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.MessageKey { + a.Key = "content" + } + return a + }})) + + hlog.SetLogger(logger) + hlog.SetOutput(buf) + + hlog.Warn(warnMsg) + assert.Equal(t, "", buf.String()) + + hlog.SetLevel(hlog.LevelInfo) + + hlog.Debug(debugMsg) + assert.Equal(t, "", buf.String()) + + hlog.Info(infoMsg) + assert.Contains(t, buf.String(), infoMsg) + assert.Contains(t, buf.String(), "content") + + buf.Reset() + hlog.SetLevel(hlog.LevelTrace) + + testCase := []struct { + levelName string + method func(...any) + msg string + }{ + { + "Trace", + hlog.Trace, + traceMsg, + }, + { + "Debug", + hlog.Debug, + debugMsg, + }, + { + "Info", + hlog.Info, + infoMsg, + }, + { + "Notice", + hlog.Notice, + noticeMsg, + }, + { + "Warn", + hlog.Warn, + warnMsg, + }, + { + "Error", + hlog.Error, + errorMsg, + }, + { + "Fatal", + hlog.Fatal, + fatalMsg, + }, + } + + for _, tc := range testCase { + tc.method(tc.msg) + assert.Contains(t, buf.String(), tc.levelName) + assert.Contains(t, buf.String(), tc.msg) + buf.Reset() + } +} + +func TestWithoutLevel(t *testing.T) { + buf := new(bytes.Buffer) + logger := NewLogger(WithHandlerOptions(&slog.HandlerOptions{AddSource: true})) + + hlog.SetLogger(logger) + hlog.SetOutput(buf) + + hlog.CtxInfof(context.TODO(), "hello %s", "otelhertz") + assert.Contains(t, buf.String(), "source") +} + +func TestWithOutput(t *testing.T) { + buf := new(bytes.Buffer) + logger := NewLogger(WithOutput(buf)) + hlog.SetLogger(logger) + + hlog.CtxErrorf(context.TODO(), errorMsg) + assert.Contains(t, buf.String(), errorMsg) +} diff --git a/log/logging/slog/option.go b/log/logging/slog/option.go new file mode 100644 index 0000000..8c47dae --- /dev/null +++ b/log/logging/slog/option.go @@ -0,0 +1,77 @@ +// Copyright 2022 CloudWeGo 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. + +package slog + +import ( + "io" + "log/slog" + "os" + + "github.com/cloudwego/hertz/pkg/common/hlog" +) + +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + level *slog.LevelVar + withLevel bool + handlerOptions *slog.HandlerOptions + withHandlerOptions bool + output io.Writer +} + +func defaultConfig() *config { + lvl := &slog.LevelVar{} + lvl.Set(tranSLevel(hlog.LevelInfo)) + + handlerOptions := &slog.HandlerOptions{ + Level: lvl, + } + return &config{ + level: lvl, + withLevel: false, + handlerOptions: handlerOptions, + withHandlerOptions: false, + output: os.Stdout, + } +} + +func WithLevel(lvl *slog.LevelVar) Option { + return option(func(cfg *config) { + cfg.level = lvl + cfg.withLevel = true + }) +} + +func WithHandlerOptions(opts *slog.HandlerOptions) Option { + return option(func(cfg *config) { + cfg.handlerOptions = opts + cfg.withHandlerOptions = true + }) +} + +func WithOutput(writer io.Writer) Option { + return option(func(cfg *config) { + cfg.output = writer + }) +} diff --git a/log/logging/slog/utils.go b/log/logging/slog/utils.go new file mode 100644 index 0000000..49d64e6 --- /dev/null +++ b/log/logging/slog/utils.go @@ -0,0 +1,44 @@ +// Copyright 2023 CloudWeGo 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. + +package slog + +import ( + "log/slog" + + "github.com/cloudwego/hertz/pkg/common/hlog" +) + +// Adapt log level to otelslog level +func tranSLevel(level hlog.Level) (lvl slog.Level) { + switch level { + case hlog.LevelTrace: + lvl = LevelTrace + case hlog.LevelDebug: + lvl = slog.LevelDebug + case hlog.LevelInfo: + lvl = slog.LevelInfo + case hlog.LevelWarn: + lvl = slog.LevelWarn + case hlog.LevelNotice: + lvl = LevelNotice + case hlog.LevelError: + lvl = slog.LevelError + case hlog.LevelFatal: + lvl = LevelFatal + default: + lvl = slog.LevelWarn + } + return +} diff --git a/log/logging/zap/go.mod b/log/logging/zap/go.mod new file mode 100644 index 0000000..8d00d72 --- /dev/null +++ b/log/logging/zap/go.mod @@ -0,0 +1,22 @@ +module github.com/cloudwego-contrib/cwgo-pkg/log/logging/zap + +go 1.21 + +require ( + github.com/cloudwego/hertz v0.9.2 + github.com/stretchr/testify v1.9.0 + go.uber.org/zap v1.24.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/goleak v1.2.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/log/logging/zap/go.sum b/log/logging/zap/go.sum new file mode 100644 index 0000000..efd59dc --- /dev/null +++ b/log/logging/zap/go.sum @@ -0,0 +1,75 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/hertz v0.9.2 h1:VbqddZ5RuvcgxzfxvXcmTiRisGYoo0+WnHGeDJKhjqI= +github.com/cloudwego/netpoll v0.6.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/log/logging/zap/logger.go b/log/logging/zap/logger.go new file mode 100644 index 0000000..26b62dd --- /dev/null +++ b/log/logging/zap/logger.go @@ -0,0 +1,268 @@ +// Copyright 2022 CloudWeGo 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. + +package zap + +import ( + "context" + "io" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var _ hlog.FullLogger = (*Logger)(nil) + +type Logger struct { + l *zap.Logger + config *config +} + +func NewLogger(opts ...Option) *Logger { + config := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(config) + } + + cores := make([]zapcore.Core, 0, len(config.coreConfigs)) + for _, coreConfig := range config.coreConfigs { + cores = append(cores, zapcore.NewCore(coreConfig.Enc, coreConfig.Ws, coreConfig.Lvl)) + } + + logger := zap.New( + zapcore.NewTee(cores[:]...), + config.zapOpts...) + + return &Logger{ + l: logger, + config: config, + } +} + +// GetExtraKeys get extraKeys from logger config +func (l *Logger) GetExtraKeys() []ExtraKey { + return l.config.extraKeys +} + +// PutExtraKeys add extraKeys after init +func (l *Logger) PutExtraKeys(keys ...ExtraKey) { + for _, k := range keys { + if !InArray(k, l.config.extraKeys) { + l.config.extraKeys = append(l.config.extraKeys, k) + } + } +} + +func (l *Logger) Log(level hlog.Level, kvs ...interface{}) { + sugar := l.l.Sugar() + switch level { + case hlog.LevelTrace, hlog.LevelDebug: + sugar.Debug(kvs...) + case hlog.LevelInfo: + sugar.Info(kvs...) + case hlog.LevelNotice, hlog.LevelWarn: + sugar.Warn(kvs...) + case hlog.LevelError: + sugar.Error(kvs...) + case hlog.LevelFatal: + sugar.Fatal(kvs...) + default: + sugar.Warn(kvs...) + } +} + +func (l *Logger) Logf(level hlog.Level, format string, kvs ...interface{}) { + logger := l.l.Sugar() + switch level { + case hlog.LevelTrace, hlog.LevelDebug: + logger.Debugf(format, kvs...) + case hlog.LevelInfo: + logger.Infof(format, kvs...) + case hlog.LevelNotice, hlog.LevelWarn: + logger.Warnf(format, kvs...) + case hlog.LevelError: + logger.Errorf(format, kvs...) + case hlog.LevelFatal: + logger.Fatalf(format, kvs...) + default: + logger.Warnf(format, kvs...) + } +} + +func (l *Logger) CtxLogf(level hlog.Level, ctx context.Context, format string, kvs ...interface{}) { + zLevel := LevelToZapLevel(level) + if !l.config.coreConfigs[0].Lvl.Enabled(zLevel) { + return + } + zapLogger := l.l + if len(l.config.extraKeys) > 0 { + for _, k := range l.config.extraKeys { + if l.config.extraKeyAsStr { + v := ctx.Value(string(k)) + if v != nil { + zapLogger = zapLogger.With(zap.Any(string(k), v)) + } + } else { + v := ctx.Value(k) + if v != nil { + zapLogger = zapLogger.With(zap.Any(string(k), v)) + } + } + } + } + log := zapLogger.Sugar() + switch level { + case hlog.LevelDebug, hlog.LevelTrace: + log.Debugf(format, kvs...) + case hlog.LevelInfo: + log.Infof(format, kvs...) + case hlog.LevelNotice, hlog.LevelWarn: + log.Warnf(format, kvs...) + case hlog.LevelError: + log.Errorf(format, kvs...) + case hlog.LevelFatal: + log.Fatalf(format, kvs...) + default: + log.Warnf(format, kvs...) + } +} + +func (l *Logger) Trace(v ...interface{}) { + l.Log(hlog.LevelTrace, v...) +} + +func (l *Logger) Debug(v ...interface{}) { + l.Log(hlog.LevelDebug, v...) +} + +func (l *Logger) Info(v ...interface{}) { + l.Log(hlog.LevelInfo, v...) +} + +func (l *Logger) Notice(v ...interface{}) { + l.Log(hlog.LevelNotice, v...) +} + +func (l *Logger) Warn(v ...interface{}) { + l.Log(hlog.LevelWarn, v...) +} + +func (l *Logger) Error(v ...interface{}) { + l.Log(hlog.LevelError, v...) +} + +func (l *Logger) Fatal(v ...interface{}) { + l.Log(hlog.LevelFatal, v...) +} + +func (l *Logger) Tracef(format string, v ...interface{}) { + l.Logf(hlog.LevelTrace, format, v...) +} + +func (l *Logger) Debugf(format string, v ...interface{}) { + l.Logf(hlog.LevelDebug, format, v...) +} + +func (l *Logger) Infof(format string, v ...interface{}) { + l.Logf(hlog.LevelInfo, format, v...) +} + +func (l *Logger) Noticef(format string, v ...interface{}) { + l.Logf(hlog.LevelWarn, format, v...) +} + +func (l *Logger) Warnf(format string, v ...interface{}) { + l.Logf(hlog.LevelWarn, format, v...) +} + +func (l *Logger) Errorf(format string, v ...interface{}) { + l.Logf(hlog.LevelError, format, v...) +} + +func (l *Logger) Fatalf(format string, v ...interface{}) { + l.Logf(hlog.LevelFatal, format, v...) +} + +func (l *Logger) CtxTracef(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxDebugf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxInfof(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelInfo, ctx, format, v...) +} + +func (l *Logger) CtxNoticef(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelWarn, ctx, format, v...) +} + +func (l *Logger) CtxWarnf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelWarn, ctx, format, v...) +} + +func (l *Logger) CtxErrorf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelError, ctx, format, v...) +} + +func (l *Logger) CtxFatalf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelFatal, ctx, format, v...) +} + +func (l *Logger) SetLevel(level hlog.Level) { + lvl := LevelToZapLevel(level) + + l.config.coreConfigs[0].Lvl = lvl + + cores := make([]zapcore.Core, 0, len(l.config.coreConfigs)) + for _, coreConfig := range l.config.coreConfigs { + cores = append(cores, zapcore.NewCore(coreConfig.Enc, coreConfig.Ws, coreConfig.Lvl)) + } + + logger := zap.New( + zapcore.NewTee(cores[:]...), + l.config.zapOpts...) + + l.l = logger +} + +func (l *Logger) SetOutput(writer io.Writer) { + l.config.coreConfigs[0].Ws = zapcore.AddSync(writer) + + cores := make([]zapcore.Core, 0, len(l.config.coreConfigs)) + for _, coreConfig := range l.config.coreConfigs { + cores = append(cores, zapcore.NewCore(coreConfig.Enc, coreConfig.Ws, coreConfig.Lvl)) + } + + logger := zap.New( + zapcore.NewTee(cores[:]...), + l.config.zapOpts...) + + l.l = logger +} + +// Logger is used to return an instance of *zap.Logger for custom fields, etc. +func (l *Logger) Logger() *zap.Logger { + return l.l +} + +func (l *Logger) Sync() { + _ = l.l.Sync() +} diff --git a/log/logging/zap/logger_test.go b/log/logging/zap/logger_test.go new file mode 100644 index 0000000..bffa7df --- /dev/null +++ b/log/logging/zap/logger_test.go @@ -0,0 +1,411 @@ +// Copyright 2022 CloudWeGo 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. + +package zap + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// testEncoderConfig encoder config for testing, copy from otelzap +func testEncoderConfig() zapcore.EncoderConfig { + return zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + NameKey: "name", + TimeKey: "ts", + CallerKey: "caller", + FunctionKey: "func", + StacktraceKey: "stacktrace", + LineEnding: "\n", + EncodeTime: zapcore.EpochTimeEncoder, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + } +} + +// humanEncoderConfig copy from otelzap +func humanEncoderConfig() zapcore.EncoderConfig { + cfg := testEncoderConfig() + cfg.EncodeTime = zapcore.ISO8601TimeEncoder + cfg.EncodeLevel = zapcore.CapitalLevelEncoder + cfg.EncodeDuration = zapcore.StringDurationEncoder + return cfg +} + +func getWriteSyncer(file string) zapcore.WriteSyncer { + _, err := os.Stat(file) + if os.IsNotExist(err) { + _ = os.MkdirAll(filepath.Dir(file), 0o744) + } + + f, _ := os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) + + return zapcore.AddSync(f) +} + +// TestLogger test logger work with otelhertz +func TestLogger(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger(WithZapOptions(zap.WithFatalHook(zapcore.WriteThenPanic))) + defer logger.Sync() + + hlog.SetLogger(logger) + hlog.SetOutput(buf) + hlog.SetLevel(hlog.LevelDebug) + + type logMap map[string]string + + logTestSlice := []logMap{ + { + "logMessage": "this is a trace log", + "formatLogMessage": "this is a trace log: %s", + "logLevel": "Trace", + "zapLogLevel": "debug", + }, + { + "logMessage": "this is a debug log", + "formatLogMessage": "this is a debug log: %s", + "logLevel": "Debug", + "zapLogLevel": "debug", + }, + { + "logMessage": "this is a info log", + "formatLogMessage": "this is a info log: %s", + "logLevel": "Info", + "zapLogLevel": "info", + }, + { + "logMessage": "this is a notice log", + "formatLogMessage": "this is a notice log: %s", + "logLevel": "Notice", + "zapLogLevel": "warn", + }, + { + "logMessage": "this is a warn log", + "formatLogMessage": "this is a warn log: %s", + "logLevel": "Warn", + "zapLogLevel": "warn", + }, + { + "logMessage": "this is a error log", + "formatLogMessage": "this is a error log: %s", + "logLevel": "Error", + "zapLogLevel": "error", + }, + { + "logMessage": "this is a fatal log", + "formatLogMessage": "this is a fatal log: %s", + "logLevel": "Fatal", + "zapLogLevel": "fatal", + }, + } + + testHertzLogger := reflect.ValueOf(logger) + + for _, v := range logTestSlice { + t.Run(v["logLevel"], func(t *testing.T) { + if v["logLevel"] == "Fatal" { + defer func() { + assert.Equal(t, "this is a fatal log", recover()) + }() + } + logFunc := testHertzLogger.MethodByName(v["logLevel"]) + logFunc.Call([]reflect.Value{ + reflect.ValueOf(v["logMessage"]), + }) + assert.Contains(t, buf.String(), v["logMessage"]) + assert.Contains(t, buf.String(), v["zapLogLevel"]) + + buf.Reset() + + logfFunc := testHertzLogger.MethodByName(fmt.Sprintf("%sf", v["logLevel"])) + logfFunc.Call([]reflect.Value{ + reflect.ValueOf(v["formatLogMessage"]), + reflect.ValueOf(v["logLevel"]), + }) + assert.Contains(t, buf.String(), fmt.Sprintf(v["formatLogMessage"], v["logLevel"])) + assert.Contains(t, buf.String(), v["zapLogLevel"]) + + buf.Reset() + + ctx := context.Background() + ctxLogfFunc := testHertzLogger.MethodByName(fmt.Sprintf("Ctx%sf", v["logLevel"])) + ctxLogfFunc.Call([]reflect.Value{ + reflect.ValueOf(ctx), + reflect.ValueOf(v["formatLogMessage"]), + reflect.ValueOf(v["logLevel"]), + }) + assert.Contains(t, buf.String(), fmt.Sprintf(v["formatLogMessage"], v["logLevel"])) + assert.Contains(t, buf.String(), v["zapLogLevel"]) + + buf.Reset() + }) + } +} + +// TestLogLevel test SetLevel +func TestLogLevel(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger() + defer logger.Sync() + + // output to buffer + logger.SetOutput(buf) + + logger.Debug("this is a debug log") + assert.False(t, strings.Contains(buf.String(), "this is a debug log")) + + logger.SetLevel(hlog.LevelDebug) + + logger.Debugf("this is a debug log %s", "msg") + assert.True(t, strings.Contains(buf.String(), "this is a debug log")) + + logger.SetLevel(hlog.LevelError) + logger.Infof("this is a debug log %s", "msg") + assert.False(t, strings.Contains(buf.String(), "this is a info log")) + + logger.Warnf("this is a warn log %s", "msg") + assert.False(t, strings.Contains(buf.String(), "this is a warn log")) + + logger.Error("this is a error log %s", "msg") + assert.True(t, strings.Contains(buf.String(), "this is a error log")) +} + +func TestWithCoreEnc(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger(WithCoreEnc(zapcore.NewConsoleEncoder(humanEncoderConfig()))) + defer logger.Sync() + + // output to buffer + logger.SetOutput(buf) + + logger.Infof("this is a info log %s", "msg") + assert.True(t, strings.Contains(buf.String(), "this is a info log")) +} + +func TestWithCoreWs(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger(WithCoreWs(zapcore.AddSync(buf))) + defer logger.Sync() + + logger.Infof("this is a info log %s", "msg") + assert.True(t, strings.Contains(buf.String(), "this is a info log")) +} + +func TestWithCoreLevel(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger(WithCoreLevel(zap.NewAtomicLevelAt(zapcore.WarnLevel))) + defer logger.Sync() + + // output to buffer + logger.SetOutput(buf) + + logger.Infof("this is a info log %s", "msg") + assert.False(t, strings.Contains(buf.String(), "this is a info log")) + + logger.Warnf("this is a warn log %s", "msg") + assert.True(t, strings.Contains(buf.String(), "this is a warn log")) +} + +// TestCoreOption test zapcore config option +func TestCoreOption(t *testing.T) { + buf := new(bytes.Buffer) + + dynamicLevel := zap.NewAtomicLevel() + + dynamicLevel.SetLevel(zap.InfoLevel) + + logger := NewLogger( + WithCores([]CoreConfig{ + { + Enc: zapcore.NewConsoleEncoder(humanEncoderConfig()), + Ws: zapcore.AddSync(os.Stdout), + Lvl: dynamicLevel, + }, + { + Enc: zapcore.NewJSONEncoder(humanEncoderConfig()), + Ws: getWriteSyncer("./all/log.log"), + Lvl: zap.NewAtomicLevelAt(zapcore.DebugLevel), + }, + { + Enc: zapcore.NewJSONEncoder(humanEncoderConfig()), + Ws: getWriteSyncer("./debug/log.log"), + Lvl: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev == zap.DebugLevel + }), + }, + { + Enc: zapcore.NewJSONEncoder(humanEncoderConfig()), + Ws: getWriteSyncer("./info/log.log"), + Lvl: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev == zap.InfoLevel + }), + }, + { + Enc: zapcore.NewJSONEncoder(humanEncoderConfig()), + Ws: getWriteSyncer("./warn/log.log"), + Lvl: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev == zap.WarnLevel + }), + }, + { + Enc: zapcore.NewJSONEncoder(humanEncoderConfig()), + Ws: getWriteSyncer("./error/log.log"), + Lvl: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { + return lev >= zap.ErrorLevel + }), + }, + }...), + ) + defer logger.Sync() + + logger.SetOutput(buf) + + logger.Debug("this is a debug log") + // test log level + assert.False(t, strings.Contains(buf.String(), "this is a debug log")) + + logger.Error("this is a warn log") + // test log level + assert.True(t, strings.Contains(buf.String(), "this is a warn log")) + // test console encoder result + assert.True(t, strings.Contains(buf.String(), "\tERROR\t")) + + logger.SetLevel(hlog.LevelDebug) + logger.Debug("this is a debug log") + assert.True(t, strings.Contains(buf.String(), "this is a debug log")) +} + +// TestCoreOption test zapcore config option +func TestZapOption(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger( + WithZapOptions(zap.AddCaller()), + ) + defer logger.Sync() + + logger.SetOutput(buf) + + logger.Debug("this is a debug log") + assert.False(t, strings.Contains(buf.String(), "this is a debug log")) + + logger.Error("this is a warn log") + // test caller in log result + assert.True(t, strings.Contains(buf.String(), "caller")) +} + +// TestWithExtraKeys test WithExtraKeys option +func TestWithExtraKeys(t *testing.T) { + buf := new(bytes.Buffer) + + log := NewLogger(WithExtraKeys([]ExtraKey{"requestId"})) + log.SetOutput(buf) + + ctx := context.WithValue(context.Background(), ExtraKey("requestId"), "123") + + log.CtxInfof(ctx, "%s log", "extra") + + var logStructMap map[string]interface{} + + err := json.Unmarshal(buf.Bytes(), &logStructMap) + + assert.Nil(t, err) + + value, ok := logStructMap["requestId"] + + assert.True(t, ok) + assert.Equal(t, value, "123") +} + +func TestPutExtraKeys(t *testing.T) { + logger := NewLogger(WithExtraKeys([]ExtraKey{"abc"})) + + assert.Contains(t, logger.GetExtraKeys(), ExtraKey("abc")) + assert.NotContains(t, logger.GetExtraKeys(), ExtraKey("def")) + + logger.PutExtraKeys("def") + assert.Contains(t, logger.GetExtraKeys(), ExtraKey("def")) +} + +func TestExtraKeyAsStr(t *testing.T) { + buf := new(bytes.Buffer) + const v = "value" + + logger := NewLogger(WithExtraKeys([]ExtraKey{"abc"})) + + logger.SetOutput(buf) + + ctx1 := context.TODO() + ctx1 = context.WithValue(ctx1, "key1", v) //nolint:staticcheck + logger.CtxErrorf(ctx1, "%s", "error") + + assert.NotContains(t, buf.String(), v) + + buf.Reset() + + strLogger := NewLogger(WithExtraKeys([]ExtraKey{"abc"}), WithExtraKeyAsStr()) + + strLogger.SetOutput(buf) + + ctx2 := context.TODO() + ctx2 = context.WithValue(ctx2, "key2", v) //nolint:staticcheck + + strLogger.CtxErrorf(ctx2, "key2", v) + + assert.Contains(t, buf.String(), v) + + buf.Reset() +} + +func BenchmarkNormal(b *testing.B) { + buf := new(bytes.Buffer) + log := NewLogger() + log.SetOutput(buf) + ctx := context.Background() + for i := 0; i < b.N; i++ { + log.CtxInfof(ctx, "normal log") + } +} + +func BenchmarkWithExtraKeys(b *testing.B) { + buf := new(bytes.Buffer) + log := NewLogger(WithExtraKeys([]ExtraKey{"requestId"})) + log.SetOutput(buf) + ctx := context.WithValue(context.Background(), ExtraKey("requestId"), "123") + for i := 0; i < b.N; i++ { + log.CtxInfof(ctx, "normal log") + } +} diff --git a/log/logging/zap/option.go b/log/logging/zap/option.go new file mode 100644 index 0000000..ffb6cde --- /dev/null +++ b/log/logging/zap/option.go @@ -0,0 +1,137 @@ +// Copyright 2022 CloudWeGo 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. + +package zap + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Option interface { + apply(cfg *config) +} + +type ExtraKey string + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type CoreConfig struct { + Enc zapcore.Encoder + Ws zapcore.WriteSyncer + Lvl zapcore.LevelEnabler +} + +type config struct { + extraKeys []ExtraKey + coreConfigs []CoreConfig + zapOpts []zap.Option + customFields []interface{} + extraKeyAsStr bool +} + +// defaultCoreConfig default zapcore config: json encoder, atomic level, stdout write syncer +func defaultCoreConfig() *CoreConfig { + // default log encoder + enc := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) + // default log level + lvl := zap.NewAtomicLevelAt(zap.InfoLevel) + // default write syncer stdout + ws := zapcore.AddSync(os.Stdout) + + return &CoreConfig{ + Enc: enc, + Ws: ws, + Lvl: lvl, + } +} + +// defaultConfig default config +func defaultConfig() *config { + return &config{ + coreConfigs: []CoreConfig{*defaultCoreConfig()}, + zapOpts: []zap.Option{}, + extraKeyAsStr: false, + } +} + +// WithCoreEnc zapcore encoder +func WithCoreEnc(enc zapcore.Encoder) Option { + return option(func(cfg *config) { + cfg.coreConfigs[0].Enc = enc + }) +} + +// WithCoreWs zapcore write syncer +func WithCoreWs(ws zapcore.WriteSyncer) Option { + return option(func(cfg *config) { + cfg.coreConfigs[0].Ws = ws + }) +} + +// WithCoreLevel zapcore log level +func WithCoreLevel(lvl zap.AtomicLevel) Option { + return option(func(cfg *config) { + cfg.coreConfigs[0].Lvl = lvl + }) +} + +// WithCores zapcore +func WithCores(coreConfigs ...CoreConfig) Option { + return option(func(cfg *config) { + cfg.coreConfigs = coreConfigs + }) +} + +// WithZapOptions add origin otelzap option +func WithZapOptions(opts ...zap.Option) Option { + return option(func(cfg *config) { + cfg.zapOpts = append(cfg.zapOpts, opts...) + }) +} + +// WithExtraKeys allow you log extra values from context +func WithExtraKeys(keys []ExtraKey) Option { + return option(func(cfg *config) { + for _, k := range keys { + if !InArray(k, cfg.extraKeys) { + cfg.extraKeys = append(cfg.extraKeys, k) + } + } + }) +} + +// WithExtraKeyAsStr convert extraKey to a string type when retrieving value from context +// Not recommended for use, only for compatibility with certain situations +// +// For more information, refer to the documentation at +// `https://pkg.go.dev/context#WithValue` +func WithExtraKeyAsStr() Option { + return option(func(cfg *config) { + cfg.extraKeyAsStr = true + }) +} + +// WithCustomFields record log with the key-value pair. +func WithCustomFields(kv ...interface{}) Option { + return option(func(cfg *config) { + cfg.customFields = append(cfg.customFields, kv...) + }) +} diff --git a/log/logging/zap/utils.go b/log/logging/zap/utils.go new file mode 100644 index 0000000..24ead9d --- /dev/null +++ b/log/logging/zap/utils.go @@ -0,0 +1,50 @@ +// Copyright 2022 CloudWeGo 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. + +package zap + +import ( + "github.com/cloudwego/hertz/pkg/common/hlog" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// InArray check if a string in a slice +func InArray(key ExtraKey, arr []ExtraKey) bool { + for _, k := range arr { + if k == key { + return true + } + } + return false +} + +func LevelToZapLevel(level hlog.Level) zapcore.Level { + var lvl zapcore.Level + switch level { + case hlog.LevelTrace, hlog.LevelDebug: + lvl = zap.DebugLevel + case hlog.LevelInfo: + lvl = zap.InfoLevel + case hlog.LevelWarn, hlog.LevelNotice: + lvl = zap.WarnLevel + case hlog.LevelError: + lvl = zap.ErrorLevel + case hlog.LevelFatal: + lvl = zap.FatalLevel + default: + lvl = zap.WarnLevel + } + return lvl +} diff --git a/log/logging/zerolog/go.mod b/log/logging/zerolog/go.mod new file mode 100644 index 0000000..545a21b --- /dev/null +++ b/log/logging/zerolog/go.mod @@ -0,0 +1,27 @@ +module github.com/cloudwego-contrib/cwgo-pkg/log/logging/zerolog + +go 1.21 + +require ( + github.com/cloudwego/hertz v0.9.2 + github.com/rs/zerolog v1.30.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/bytedance/sonic v1.11.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.5.0 // indirect + golang.org/x/sys v0.21.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/log/logging/zerolog/levels.go b/log/logging/zerolog/levels.go new file mode 100644 index 0000000..c21a7ef --- /dev/null +++ b/log/logging/zerolog/levels.go @@ -0,0 +1,65 @@ +/* + * Copyright 2022 CloudWeGo 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. + */ + +package zerolog + +import ( + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/rs/zerolog" +) + +var ( + zerologLevels = map[hlog.Level]zerolog.Level{ + hlog.LevelTrace: zerolog.TraceLevel, + hlog.LevelDebug: zerolog.DebugLevel, + hlog.LevelInfo: zerolog.InfoLevel, + hlog.LevelWarn: zerolog.WarnLevel, + hlog.LevelNotice: zerolog.WarnLevel, + hlog.LevelError: zerolog.ErrorLevel, + hlog.LevelFatal: zerolog.FatalLevel, + } + + logginglevel = map[zerolog.Level]hlog.Level{ + zerolog.TraceLevel: hlog.LevelTrace, + zerolog.DebugLevel: hlog.LevelDebug, + zerolog.InfoLevel: hlog.LevelInfo, + zerolog.WarnLevel: hlog.LevelWarn, + zerolog.ErrorLevel: hlog.LevelError, + zerolog.FatalLevel: hlog.LevelFatal, + } +) + +// matchHlogLevel map hlog.Level to otelzerolog.Level +func matchlogLevel(level hlog.Level) zerolog.Level { + zlvl, found := zerologLevels[level] + + if found { + return zlvl + } + + return zerolog.WarnLevel // Default level +} + +// matchZerologLevel map otelzerolog.Level to hlog.Level +func matchZerologLevel(level zerolog.Level) hlog.Level { + hlvl, found := logginglevel[level] + + if found { + return hlvl + } + + return hlog.LevelWarn // Default level +} diff --git a/log/logging/zerolog/levels_test.go b/log/logging/zerolog/levels_test.go new file mode 100644 index 0000000..f97d652 --- /dev/null +++ b/log/logging/zerolog/levels_test.go @@ -0,0 +1,44 @@ +/* + * Copyright 2022 CloudWeGo 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. + */ + +package zerolog + +import ( + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestMatchlogLevel(t *testing.T) { + assert.Equal(t, zerolog.TraceLevel, matchlogLevel(hlog.LevelTrace)) + assert.Equal(t, zerolog.DebugLevel, matchlogLevel(hlog.LevelDebug)) + assert.Equal(t, zerolog.InfoLevel, matchlogLevel(hlog.LevelInfo)) + assert.Equal(t, zerolog.WarnLevel, matchlogLevel(hlog.LevelWarn)) + assert.Equal(t, zerolog.ErrorLevel, matchlogLevel(hlog.LevelError)) + assert.Equal(t, zerolog.FatalLevel, matchlogLevel(hlog.LevelFatal)) +} + +func TestMatchZerologLevel(t *testing.T) { + assert.Equal(t, hlog.LevelTrace, matchZerologLevel(zerolog.TraceLevel)) + assert.Equal(t, hlog.LevelDebug, matchZerologLevel(zerolog.DebugLevel)) + assert.Equal(t, hlog.LevelInfo, matchZerologLevel(zerolog.InfoLevel)) + assert.Equal(t, hlog.LevelWarn, matchZerologLevel(zerolog.WarnLevel)) + assert.Equal(t, hlog.LevelError, matchZerologLevel(zerolog.ErrorLevel)) + assert.Equal(t, hlog.LevelFatal, matchZerologLevel(zerolog.FatalLevel)) +} diff --git a/log/logging/zerolog/logger.go b/log/logging/zerolog/logger.go new file mode 100644 index 0000000..022b115 --- /dev/null +++ b/log/logging/zerolog/logger.go @@ -0,0 +1,278 @@ +/* + * Copyright 2022 CloudWeGo 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. + */ + +package zerolog + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/rs/zerolog" +) + +var _ hlog.FullLogger = (*Logger)(nil) + +// Logger is a wrapper around `zerolog.Logger` that provides an implementation of `log.FullLogger` interface +type Logger struct { + log zerolog.Logger + out io.Writer + level zerolog.Level + options []Opt +} + +// ConsoleWriter parses the JSON input and writes it in an +// (optionally) colorized, human-friendly format to Out. +type ConsoleWriter = zerolog.ConsoleWriter + +// MultiLevelWriter may be used to send the log message to multiple outputs. +func MultiLevelWriter(writers ...io.Writer) zerolog.LevelWriter { + return zerolog.MultiLevelWriter(writers...) +} + +// New returns a new Logger instance +func New(options ...Opt) *Logger { + return newLogger(zerolog.New(os.Stdout), options) +} + +// From returns a new Logger instance using an existing logger +func From(log zerolog.Logger, options ...Opt) *Logger { + return newLogger(log, options) +} + +// GetLogger returns the default logger instance +func GetLogger() (Logger, error) { + defaultLogger := hlog.DefaultLogger() + + if logger, ok := defaultLogger.(*Logger); ok { + return *logger, nil + } + + return Logger{}, errors.New("hlog.DefaultLogger is not a zerolog logger") +} + +// SetLevel setting logging level for logger +func (l *Logger) SetLevel(level hlog.Level) { + lvl := matchlogLevel(level) + l.level = lvl + l.log = l.log.Level(lvl) +} + +// SetOutput setting output for logger +func (l *Logger) SetOutput(writer io.Writer) { + l.out = writer + l.log = l.log.Output(writer) +} + +// WithContext returns context with logger attached +func (l *Logger) WithContext(ctx context.Context) context.Context { + return l.log.WithContext(ctx) +} + +// WithField appends a field to the logger +func (l *Logger) WithField(key string, value interface{}) Logger { + l.log = l.log.With().Interface(key, value).Logger() + return *l +} + +// Unwrap returns the underlying otelzerolog logger +func (l *Logger) Unwrap() zerolog.Logger { + return l.log +} + +// Log log using otelzerolog logger with specified level +func (l *Logger) Log(level hlog.Level, kvs ...interface{}) { + switch level { + case hlog.LevelTrace, hlog.LevelDebug: + l.log.Debug().Msg(fmt.Sprint(kvs...)) + case hlog.LevelInfo: + l.log.Info().Msg(fmt.Sprint(kvs...)) + case hlog.LevelNotice, hlog.LevelWarn: + l.log.Warn().Msg(fmt.Sprint(kvs...)) + case hlog.LevelError: + l.log.Error().Msg(fmt.Sprint(kvs...)) + case hlog.LevelFatal: + l.log.Fatal().Msg(fmt.Sprint(kvs...)) + default: + l.log.Warn().Msg(fmt.Sprint(kvs...)) + } +} + +// Logf log using otelzerolog logger with specified level and formatting +func (l *Logger) Logf(level hlog.Level, format string, kvs ...interface{}) { + switch level { + case hlog.LevelTrace, hlog.LevelDebug: + l.log.Debug().Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelInfo: + l.log.Info().Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelNotice, hlog.LevelWarn: + l.log.Warn().Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelError: + l.log.Error().Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelFatal: + l.log.Fatal().Msg(fmt.Sprintf(format, kvs...)) + default: + l.log.Warn().Msg(fmt.Sprintf(format, kvs...)) + } +} + +// CtxLogf log with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxLogf(level hlog.Level, ctx context.Context, format string, kvs ...interface{}) { + logger := l.Unwrap() + // todo add hook + switch level { + case hlog.LevelTrace, hlog.LevelDebug: + logger.Debug().Ctx(ctx).Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelInfo: + logger.Info().Ctx(ctx).Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelNotice, hlog.LevelWarn: + logger.Warn().Ctx(ctx).Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelError: + logger.Error().Ctx(ctx).Msg(fmt.Sprintf(format, kvs...)) + case hlog.LevelFatal: + logger.Fatal().Ctx(ctx).Msg(fmt.Sprintf(format, kvs...)) + default: + logger.Warn().Ctx(ctx).Msg(fmt.Sprintf(format, kvs...)) + } +} + +// Trace logs a message at trace level. +func (l *Logger) Trace(v ...interface{}) { + l.Log(hlog.LevelTrace, v...) +} + +// Debug logs a message at debug level. +func (l *Logger) Debug(v ...interface{}) { + l.Log(hlog.LevelDebug, v...) +} + +// Info logs a message at info level. +func (l *Logger) Info(v ...interface{}) { + l.Log(hlog.LevelInfo, v...) +} + +// Notice logs a message at notice level. +func (l *Logger) Notice(v ...interface{}) { + l.Log(hlog.LevelNotice, v...) +} + +// Warn logs a message at warn level. +func (l *Logger) Warn(v ...interface{}) { + l.Log(hlog.LevelWarn, v...) +} + +// Error logs a message at error level. +func (l *Logger) Error(v ...interface{}) { + l.Log(hlog.LevelError, v...) +} + +// Fatal logs a message at fatal level. +func (l *Logger) Fatal(v ...interface{}) { + l.Log(hlog.LevelFatal, v...) +} + +// Tracef logs a formatted message at trace level. +func (l *Logger) Tracef(format string, v ...interface{}) { + l.Logf(hlog.LevelTrace, format, v...) +} + +// Debugf logs a formatted message at debug level. +func (l *Logger) Debugf(format string, v ...interface{}) { + l.Logf(hlog.LevelDebug, format, v...) +} + +// Infof logs a formatted message at info level. +func (l *Logger) Infof(format string, v ...interface{}) { + l.Logf(hlog.LevelInfo, format, v...) +} + +// Noticef logs a formatted message at notice level. +func (l *Logger) Noticef(format string, v ...interface{}) { + l.Logf(hlog.LevelWarn, format, v...) +} + +// Warnf logs a formatted message at warn level. +func (l *Logger) Warnf(format string, v ...interface{}) { + l.Logf(hlog.LevelWarn, format, v...) +} + +// Errorf logs a formatted message at error level. +func (l *Logger) Errorf(format string, v ...interface{}) { + l.Logf(hlog.LevelError, format, v...) +} + +// Fatalf logs a formatted message at fatal level. +func (l *Logger) Fatalf(format string, v ...interface{}) { + l.Logf(hlog.LevelError, format, v...) +} + +// CtxTracef logs a message at trace level with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxTracef(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelTrace, ctx, format, v...) +} + +// CtxDebugf logs a message at debug level with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxDebugf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelDebug, ctx, format, v...) +} + +// CtxInfof logs a message at info level with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxInfof(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelInfo, ctx, format, v...) +} + +// CtxNoticef logs a message at notice level with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxNoticef(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelNotice, ctx, format, v...) +} + +// CtxWarnf logs a message at warn level with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxWarnf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelWarn, ctx, format, v...) +} + +// CtxErrorf logs a message at error level with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxErrorf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelError, ctx, format, v...) +} + +// CtxFatalf logs a message at fatal level with logger associated with context. +// If no logger is associated, DefaultContextLogger is used, unless DefaultContextLogger is nil, in which case a disabled logger is used. +func (l *Logger) CtxFatalf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelFatal, ctx, format, v...) +} + +func newLogger(log zerolog.Logger, options []Opt) *Logger { + opts := newOptions(log, options) + + return &Logger{ + log: opts.context.Logger(), + out: nil, + level: opts.level, + options: options, + } +} diff --git a/log/logging/zerolog/logger_test.go b/log/logging/zerolog/logger_test.go new file mode 100644 index 0000000..de2ec25 --- /dev/null +++ b/log/logging/zerolog/logger_test.go @@ -0,0 +1,329 @@ +/* + * Copyright 2022 CloudWeGo 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. + */ + +package zerolog + +import ( + "bytes" + "context" + "encoding/json" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" +) + +func TestFrom(t *testing.T) { + b := &bytes.Buffer{} + + zl := zerolog.New(b).With().Str("key", "test").Logger() + l := From(zl) + + l.Info("foo") + + assert.Equal( + t, + `{"level":"info","key":"test","message":"foo"} +`, + b.String(), + ) +} + +func TestGetLogger_notSet(t *testing.T) { + _, err := GetLogger() + + assert.Error(t, err) + assert.Equal(t, "hlog.DefaultLogger is not a zerolog logger", err.Error()) +} + +func TestGetLogger(t *testing.T) { + hlog.SetLogger(New()) + logger, err := GetLogger() + + assert.NoError(t, err) + assert.IsType(t, Logger{}, logger) +} + +func TestWithContext(t *testing.T) { + ctx := context.Background() + l := New() + c := l.WithContext(ctx) + + assert.NotNil(t, c) + assert.IsType(t, zerolog.Ctx(c), &zerolog.Logger{}) +} + +func TestLoggerWithField(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + l.WithField("service", "logging") + + l.Info("foobar") + + type Log struct { + Level string `json:"level"` + Service string `json:"service"` + Message string `json:"message"` + } + + log := &Log{} + + err := json.Unmarshal(b.Bytes(), log) + + println(b.String()) + assert.NoError(t, err) + assert.Equal(t, "logging", log.Service) +} + +func TestUnwrap(t *testing.T) { + l := New() + + logger := l.Unwrap() + + assert.NotNil(t, logger) + assert.IsType(t, zerolog.Logger{}, logger) +} + +func TestLog(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + + l.Trace("foo") + assert.Equal( + t, + `{"level":"debug","message":"foo"} +`, + b.String(), + ) + + b.Reset() + l.Debug("foo") + assert.Equal( + t, + `{"level":"debug","message":"foo"} +`, + b.String(), + ) + + b.Reset() + l.Info("foo") + assert.Equal( + t, + `{"level":"info","message":"foo"} +`, + b.String(), + ) + + b.Reset() + l.Notice("foo") + assert.Equal( + t, + `{"level":"warn","message":"foo"} +`, + b.String(), + ) + + b.Reset() + l.Warn("foo") + assert.Equal( + t, + `{"level":"warn","message":"foo"} +`, + b.String(), + ) + + b.Reset() + l.Error("foo") + assert.Equal( + t, + `{"level":"error","message":"foo"} +`, + b.String(), + ) +} + +func TestLogf(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + + l.Tracef("foo%s", "bar") + assert.Equal( + t, + `{"level":"debug","message":"foobar"} +`, + b.String(), + ) + + b.Reset() + l.Debugf("foo%s", "bar") + assert.Equal( + t, + `{"level":"debug","message":"foobar"} +`, + b.String(), + ) + + b.Reset() + l.Infof("foo%s", "bar") + assert.Equal( + t, + `{"level":"info","message":"foobar"} +`, + b.String(), + ) + + b.Reset() + l.Noticef("foo%s", "bar") + assert.Equal( + t, + `{"level":"warn","message":"foobar"} +`, + b.String(), + ) + + b.Reset() + l.Warnf("foo%s", "bar") + assert.Equal( + t, + `{"level":"warn","message":"foobar"} +`, + b.String(), + ) + + b.Reset() + l.Errorf("foo%s", "bar") + assert.Equal( + t, + `{"level":"error","message":"foobar"} +`, + b.String(), + ) +} + +func TestCtxTracef(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + ctx := l.log.WithContext(context.Background()) + + l.CtxTracef(ctx, "foo%s", "bar") + assert.Equal( + t, + `{"level":"debug","message":"foobar"} +`, + b.String(), + ) + assert.NotNil(t, log.Ctx(ctx)) +} + +func TestCtxDebugf(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + ctx := l.log.WithContext(context.Background()) + + l.CtxDebugf(ctx, "foo%s", "bar") + assert.Equal( + t, + `{"level":"debug","message":"foobar"} +`, + b.String(), + ) + assert.NotNil(t, log.Ctx(ctx)) +} + +func TestCtxInfof(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + ctx := l.log.WithContext(context.Background()) + + l.CtxInfof(ctx, "foo%s", "bar") + assert.Equal( + t, + `{"level":"info","message":"foobar"} +`, + b.String(), + ) + assert.NotNil(t, log.Ctx(ctx)) +} + +func TestCtxNoticef(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + ctx := l.log.WithContext(context.Background()) + + l.CtxNoticef(ctx, "foo%s", "bar") + assert.Equal( + t, + `{"level":"warn","message":"foobar"} +`, + b.String(), + ) + assert.NotNil(t, log.Ctx(ctx)) +} + +func TestCtxWarnf(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + ctx := l.log.WithContext(context.Background()) + + l.CtxWarnf(ctx, "foo%s", "bar") + assert.Equal( + t, + `{"level":"warn","message":"foobar"} +`, + b.String(), + ) + assert.NotNil(t, log.Ctx(ctx)) +} + +func TestCtxErrorf(t *testing.T) { + b := &bytes.Buffer{} + l := New() + l.SetOutput(b) + ctx := l.log.WithContext(context.Background()) + + l.CtxErrorf(ctx, "foo%s", "bar") + assert.Equal( + t, + `{"level":"error","message":"foobar"} +`, + b.String(), + ) + assert.NotNil(t, log.Ctx(ctx)) +} + +func TestSetLevel(t *testing.T) { + l := New() + + l.SetLevel(hlog.LevelDebug) + assert.Equal(t, l.log.GetLevel(), zerolog.DebugLevel) + + l.SetLevel(hlog.LevelDebug) + assert.Equal(t, l.log.GetLevel(), zerolog.DebugLevel) + + l.SetLevel(hlog.LevelError) + assert.Equal(t, l.log.GetLevel(), zerolog.ErrorLevel) +} diff --git a/log/logging/zerolog/options.go b/log/logging/zerolog/options.go new file mode 100644 index 0000000..9e11530 --- /dev/null +++ b/log/logging/zerolog/options.go @@ -0,0 +1,122 @@ +/* + * Copyright 2022 CloudWeGo 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. + */ + +package zerolog + +import ( + "io" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/rs/zerolog" +) + +type ( + Options struct { + context zerolog.Context + level zerolog.Level + } + + Opt func(opts *Options) +) + +func newOptions(log zerolog.Logger, options []Opt) *Options { + opts := &Options{ + context: log.With(), + level: log.GetLevel(), + } + + for _, set := range options { + set(opts) + } + + return opts +} + +// WithOutput allows to specify the output of the logger. By default, it is set to os.Stdout. +func WithOutput(out io.Writer) Opt { + return func(opts *Options) { + opts.context = opts.context.Logger().Output(out).With() + } +} + +// WithLevel allows to specify the level of the logger. By default, it is set to WarnLevel. +func WithLevel(level hlog.Level) Opt { + lvl := matchlogLevel(level) + return func(opts *Options) { + opts.context = opts.context.Logger().Level(lvl).With() + opts.level = lvl + } +} + +// WithField adds a field to the logger's context +func WithField(name string, value interface{}) Opt { + return func(opts *Options) { + opts.context = opts.context.Interface(name, value) + } +} + +// WithFields adds fields to the logger's context +func WithFields(fields map[string]interface{}) Opt { + return func(opts *Options) { + opts.context = opts.context.Fields(fields) + } +} + +// WithTimestamp adds a timestamp field to the logger's context +func WithTimestamp() Opt { + return func(opts *Options) { + opts.context = opts.context.Timestamp() + } +} + +// WithFormattedTimestamp adds a formatted timestamp field to the logger's context +func WithFormattedTimestamp(format string) Opt { + zerolog.TimeFieldFormat = format + return func(opts *Options) { + opts.context = opts.context.Timestamp() + } +} + +// WithCaller adds a caller field to the logger's context +func WithCaller() Opt { + return func(opts *Options) { + opts.context = opts.context.Caller() + } +} + +// WithCallerSkipFrameCount adds a caller field to the logger's context +// The specified skipFrameCount int will override the global CallerSkipFrameCount for this context's respective logger. +// If set to -1 the global CallerSkipFrameCount will be used. +func WithCallerSkipFrameCount(skipFrameCount int) Opt { + return func(opts *Options) { + opts.context = opts.context.CallerWithSkipFrameCount(skipFrameCount) + } +} + +// WithHook adds a hook to the logger's context +func WithHook(hook zerolog.Hook) Opt { + return func(opts *Options) { + opts.context = opts.context.Logger().Hook(hook).With() + } +} + +// WithHookFunc adds hook function to the logger's context +func WithHookFunc(hook zerolog.HookFunc) Opt { + return func(opts *Options) { + opts.context = opts.context.Logger().Hook(hook).With() + } +} diff --git a/log/logging/zerolog/options_test.go b/log/logging/zerolog/options_test.go new file mode 100644 index 0000000..779c7f5 --- /dev/null +++ b/log/logging/zerolog/options_test.go @@ -0,0 +1,247 @@ +/* + * Copyright 2022 CloudWeGo 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. + */ + +package zerolog + +import ( + "bytes" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/cloudwego/hertz/pkg/common/json" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestWithOutput(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithOutput(b)) + + l.Info("foobar") + + assert.Equal( + t, + `{"level":"info","message":"foobar"} +`, + b.String(), + ) +} + +func TestWithCaller(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithCaller()) + l.SetOutput(b) + + l.Info("foobar") + + type Log struct { + Level string `json:"level"` + Caller string `json:"caller"` + Message string `json:"message"` + } + + log := &Log{} + + err := json.Unmarshal(b.Bytes(), log) + + assert.NoError(t, err) + + segments := strings.Split(log.Caller, ":") + filePath := filepath.Base(segments[0]) + + assert.Equal(t, filePath, "logger.go") +} + +func TestWithCallerSkipFrameCount(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithCallerSkipFrameCount(5)) + l.SetOutput(b) + hlog.SetLogger(l) + hlog.Info("foobar") + + type Log struct { + Level string `json:"level"` + Caller string `json:"caller"` + Message string `json:"message"` + } + + log := &Log{} + + err := json.Unmarshal(b.Bytes(), log) + + assert.NoError(t, err) + + segments := strings.Split(log.Caller, ":") + filePath := filepath.Base(segments[0]) + + assert.Equal(t, filePath, "options_test.go") +} + +func TestWithField(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithField("service", "logging")) + l.SetOutput(b) + + l.Info("foobar") + + type Log struct { + Level string `json:"level"` + Service string `json:"service"` + Message string `json:"message"` + } + + log := &Log{} + + err := json.Unmarshal(b.Bytes(), log) + + assert.NoError(t, err) + assert.Equal(t, log.Service, "logging") +} + +func TestWithFields(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithFields(map[string]interface{}{ + "host": "localhost", + "port": 8080, + })) + l.SetOutput(b) + + l.Info("foobar") + + type Log struct { + Level string `json:"level"` + Host string `json:"host"` + Port int `json:"port"` + Message string `json:"message"` + } + + log := &Log{} + + err := json.Unmarshal(b.Bytes(), log) + + assert.NoError(t, err) + assert.Equal(t, log.Host, "localhost") + assert.Equal(t, log.Port, 8080) +} + +type ( + Hook struct { + logs []HookLog + } + + HookLog struct { + level zerolog.Level + message string + } +) + +func (h *Hook) Run(e *zerolog.Event, level zerolog.Level, message string) { + h.logs = append(h.logs, HookLog{ + level: level, + message: message, + }) +} + +func TestWithHook(t *testing.T) { + b := &bytes.Buffer{} + h := &Hook{} + l := New(WithHook(h)) + l.SetOutput(b) + + l.Info("Foo") + l.Warn("Bar") + + assert.Len(t, h.logs, 2) + assert.Equal(t, h.logs[0].level, zerolog.InfoLevel) + assert.Equal(t, h.logs[0].message, "Foo") + assert.Equal(t, h.logs[1].level, zerolog.WarnLevel) + assert.Equal(t, h.logs[1].message, "Bar") +} + +func TestWithHookFunc(t *testing.T) { + b := &bytes.Buffer{} + logs := make([]HookLog, 0, 2) + l := New(WithHookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + logs = append(logs, HookLog{ + level: level, + message: message, + }) + })) + l.SetOutput(b) + + l.Info("Foo") + l.Warn("Bar") + + assert.Len(t, logs, 2) + assert.Equal(t, logs[0].level, zerolog.InfoLevel) + assert.Equal(t, logs[0].message, "Foo") + assert.Equal(t, logs[1].level, zerolog.WarnLevel) + assert.Equal(t, logs[1].message, "Bar") +} + +func TestWithLevel(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithLevel(hlog.LevelInfo)) + l.SetOutput(b) + + l.Debug("Test") + + assert.Equal(t, b.String(), "") + + l.Info("foobar") + + assert.Equal(t, b.String(), `{"level":"info","message":"foobar"} +`) +} + +type Log struct { + Level string `json:"level"` + Message string `json:"message"` + Time time.Time `json:"time"` +} + +func TestWithTimestamp(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithTimestamp()) + l.SetOutput(b) + + l.Info("foobar") + + log := &Log{} + + err := json.Unmarshal(b.Bytes(), log) + + assert.NoError(t, err) + assert.NotEmpty(t, log.Time) +} + +func TestWithFormattedTimestamp(t *testing.T) { + b := &bytes.Buffer{} + l := New(WithFormattedTimestamp(time.RFC3339Nano)) + l.SetOutput(b) + + l.Info("foobar") + + log := &Log{} + err := json.Unmarshal(b.Bytes(), log) + + assert.NoError(t, err) + assert.NotEmpty(t, log.Time) +} diff --git a/telemetry/go.mod b/telemetry/go.mod new file mode 100644 index 0000000..e97b343 --- /dev/null +++ b/telemetry/go.mod @@ -0,0 +1,89 @@ +module github.com/cloudwego-contrib/cwgo-pkg/telemetry + +go 1.21 + +require ( + github.com/bytedance/gopkg v0.0.0-20230728082804-614d0af6619b + github.com/cloudwego/kitex v0.9.1 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0 + go.opentelemetry.io/contrib/propagators/b3 v1.20.0 + go.opentelemetry.io/contrib/propagators/ot v1.20.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 + go.opentelemetry.io/otel/metric v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/sdk/metric v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 +) + +require ( + github.com/cloudwego/hertz v0.9.2 + github.com/prometheus/client_golang v1.19.1 + go.opentelemetry.io/otel/exporters/prometheus v0.50.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 +) + +require ( + github.com/apache/thrift v0.13.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect + github.com/bytedance/sonic v1.11.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/choleraehyq/pid v0.0.18 // indirect + github.com/cloudwego/configmanager v0.2.0 // indirect + github.com/cloudwego/dynamicgo v0.2.0 // indirect + github.com/cloudwego/fastpb v0.0.4 // indirect + github.com/cloudwego/frugal v0.1.14 // indirect + github.com/cloudwego/localsession v0.0.2 // indirect + github.com/cloudwego/netpoll v0.6.0 // indirect + github.com/cloudwego/thriftgo v0.3.6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/pprof v0.0.0-20230509042627-b1315fad0c5a // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect + github.com/henrylee2cn/ameda v1.4.10 // indirect + github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect + github.com/jhump/protoreflect v1.8.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/gls v0.0.0-20220109145502-612d0167dce5 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nyaruka/phonenumbers v1.0.55 // indirect + github.com/oleiade/lane v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.5.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/telemetry/instrumentation/internal/context.go b/telemetry/instrumentation/internal/context.go new file mode 100644 index 0000000..8dfb928 --- /dev/null +++ b/telemetry/instrumentation/internal/context.go @@ -0,0 +1,58 @@ +// Copyright 2022 CloudWeGo 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. + +package internal + +import ( + "context" + + oteltrace "go.opentelemetry.io/otel/trace" +) + +type traceCarrierContextKeyType struct{} + +var traceCarrierContextKey traceCarrierContextKeyType + +type TraceCarrier struct { + tracer oteltrace.Tracer + span oteltrace.Span +} + +func WithTraceCarrier(ctx context.Context, tc *TraceCarrier) context.Context { + return context.WithValue(ctx, traceCarrierContextKey, tc) +} + +func TraceCarrierFromContext(ctx context.Context) *TraceCarrier { + if tc := ctx.Value(traceCarrierContextKey); tc != nil { + return tc.(*TraceCarrier) + } + + return nil +} + +func (t *TraceCarrier) Tracer() oteltrace.Tracer { + return t.tracer +} + +func (t *TraceCarrier) SetTracer(tracer oteltrace.Tracer) { + t.tracer = tracer +} + +func (t *TraceCarrier) Span() oteltrace.Span { + return t.span +} + +func (t *TraceCarrier) SetSpan(span oteltrace.Span) { + t.span = span +} diff --git a/telemetry/instrumentation/otelhertz/events.go b/telemetry/instrumentation/otelhertz/events.go new file mode 100644 index 0000000..96574f5 --- /dev/null +++ b/telemetry/instrumentation/otelhertz/events.go @@ -0,0 +1,50 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "github.com/cloudwego/hertz/pkg/common/tracer/stats" + "github.com/cloudwego/hertz/pkg/common/tracer/traceinfo" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var commonEvents = map[string]stats.Event{ + "http_start": stats.HTTPStart, + "http_finish": stats.HTTPFinish, + "server_handle_start": stats.ServerHandleStart, + "server_handle_finish": stats.ServerHandleFinish, + "read_header_start": stats.ReadHeaderStart, + "read_header_finish": stats.ReadHeaderFinish, + "read_body_start": stats.ReadBodyStart, + "read_body_finish": stats.ReadBodyFinish, + "write_start": stats.WriteStart, + "write_finish": stats.WriteFinish, +} + +func injectStatsEventsToSpan(span trace.Span, st traceinfo.HTTPStats) { + for name, event := range commonEvents { + if gotEvent := st.GetEvent(event); gotEvent != nil { + attrs := []attribute.KeyValue{attribute.Int("event.status", int(gotEvent.Status()))} + if gotEvent.Info() != "" { + attrs = append(attrs, attribute.String("event.info", gotEvent.Info())) + } + span.AddEvent(name, + trace.WithTimestamp(gotEvent.Time()), + trace.WithAttributes(attrs...), + ) + } + } +} diff --git a/telemetry/instrumentation/otelhertz/example_test.go b/telemetry/instrumentation/otelhertz/example_test.go new file mode 100644 index 0000000..50c82cc --- /dev/null +++ b/telemetry/instrumentation/otelhertz/example_test.go @@ -0,0 +1,68 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "context" + "testing" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelhertz/testutil" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func TestMetricsExample(t *testing.T) { + // test util + tracerProvider, registry, measureClient, measureServer := testutil.OtelTestProvider() + defer func(tracerProvider *sdktrace.TracerProvider, ctx context.Context) { + _ = tracerProvider.Shutdown(ctx) + }(tracerProvider, context.Background()) + + // server example + tracer, cfg := NewServerOption(WithMeasure(measureServer)) + h := server.Default(tracer, server.WithHostPorts(":39888")) + h.Use(ServerMiddleware(cfg)) + h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { + hlog.CtxDebugf(c, "message received successfully") + ctx.JSON(consts.StatusOK, "pong") + }) + go h.Spin() + + <-time.After(time.Millisecond * 500) + + // client example + c, _ := client.NewClient() + c.Use(ClientMiddleware(WithMeasure(measureClient))) + _, body, err := c.Get(context.Background(), nil, "http://localhost:39888/ping?foo=bar") + require.NoError(t, err) + assert.NotNil(t, body) + + // test client returns error + _, _, err = c.Get(context.Background(), nil, "http://localhost:39887/ping?foo=bar") + assert.NotNil(t, err) + + testerror := testutil.GatherAndCompare( + registry, "testdata/hertz_request_metrics.txt", + "http_server_request_count_total", "http_client_request_count_total") + // diff meter + assert.NoError(t, testerror) +} diff --git a/telemetry/instrumentation/otelhertz/hertz_tracer.go b/telemetry/instrumentation/otelhertz/hertz_tracer.go new file mode 100644 index 0000000..5725bbe --- /dev/null +++ b/telemetry/instrumentation/otelhertz/hertz_tracer.go @@ -0,0 +1,137 @@ +// Copyright 2022 CloudWeGo 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. +// 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 otelhertz + +import ( + "context" + "strconv" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/internal" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + cwmetric "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/common/adaptor" + "github.com/cloudwego/hertz/pkg/common/tracer" + "github.com/cloudwego/hertz/pkg/common/tracer/stats" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" + "go.opentelemetry.io/otel/trace" +) + +var _ tracer.Tracer = (*HertzTracer)(nil) + +const requestContextKey = "requestContext" + +type HertzTracer struct { + measure cwmetric.Measure + cfg *Config +} + +func (h HertzTracer) Start(ctx context.Context, c *app.RequestContext) context.Context { + if h.cfg.shouldIgnore(ctx, c) { + return ctx + } + tc := &internal.TraceCarrier{} + tc.SetTracer(h.cfg.tracer) + + return internal.WithTraceCarrier(ctx, tc) +} + +func (h HertzTracer) Finish(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, requestContextKey, c) + ti := c.GetTraceInfo() + st := ti.Stats() + + if st.Level() == stats.LevelDisabled { + return + } + + httpStart := st.GetEvent(stats.HTTPStart) + if httpStart == nil { + return + } + elapsedTime := float64(st.GetEvent(stats.HTTPFinish).Time().Sub(httpStart.Time())) / float64(time.Millisecond) + labels := []label.CwLabel{ + { + Key: semantic.LabelStatusCode, + Value: defaultValIfEmpty(strconv.Itoa(c.Response.Header.StatusCode()), semantic.UnknownLabelValue), + }, + { + Key: semantic.LabelPath, + Value: defaultValIfEmpty(c.FullPath(), semantic.UnknownLabelValue), + }, + { + Key: semantic.LabelHttpMethodKey, + Value: defaultValIfEmpty(string(c.Request.Method()), semantic.UnknownLabelValue), + }, + } + + if h.cfg.labelFunc != nil { + labels = append(labels, h.cfg.labelFunc(c)...) + } + + tc := internal.TraceCarrierFromContext(ctx) + var span trace.Span + if tc != nil && tc.Span() != nil && tc.Span().IsRecording() { + span = tc.Span() + // span attributes from original http request + if httpReq, err := adaptor.GetCompatRequest(c.GetRequest()); err == nil { + span.SetAttributes(semconv.NetAttributesFromHTTPRequest("tcp", httpReq)...) + span.SetAttributes(semconv.EndUserAttributesFromHTTPRequest(httpReq)...) + span.SetAttributes(semconv.HTTPServerAttributesFromHTTPRequest("", h.cfg.serverHttpRouteFormatter(c), httpReq)...) + } + + // span attributes + attrs := []attribute.KeyValue{ + semconv.HTTPURLKey.String(c.URI().String()), + semconv.NetPeerIPKey.String(c.ClientIP()), + } + span.SetAttributes(attrs...) + + injectStatsEventsToSpan(span, st) + + if panicMsg, panicStack, httpErr := parseHTTPError(ti); httpErr != nil || len(panicMsg) > 0 { + recordErrorSpanWithStack(span, httpErr, panicMsg, panicStack) + } + + span.End(trace.WithTimestamp(getEndTimeOrNow(ti))) + + metricsAttributes := semantic.ExtractMetricsAttributesFromSpan(span) + + labels = append(labels, label.ToCwLabelsFromOtels(metricsAttributes)...) + } + h.measure.Inc(ctx, semantic.HTTPCounter, labels...) + h.measure.Record(ctx, semantic.HTTPLatency, elapsedTime, labels...) +} + +func defaultValIfEmpty(val, def string) string { + if val == "" { + return def + } + return val +} diff --git a/telemetry/instrumentation/otelhertz/middleware.go b/telemetry/instrumentation/otelhertz/middleware.go new file mode 100644 index 0000000..bd20cb1 --- /dev/null +++ b/telemetry/instrumentation/otelhertz/middleware.go @@ -0,0 +1,169 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "context" + "strconv" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/internal" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/common/adaptor" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/common/tracer/stats" + "github.com/cloudwego/hertz/pkg/protocol" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.10.0" + oteltrace "go.opentelemetry.io/otel/trace" +) + +type StringHeader protocol.RequestHeader + +// Visit implements the metainfo.HTTPHeaderCarrier interface. +func (sh *StringHeader) Visit(f func(k, v string)) { + (*protocol.RequestHeader)(sh).VisitAll( + func(key, value []byte) { + f(string(key), string(value)) + }) +} + +func ClientMiddleware(opts ...Option) client.Middleware { + cfg := NewConfig(opts...) + + return func(next client.Endpoint) client.Endpoint { + return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) { + if ctx == nil { + ctx = context.Background() + } + + // trace start + start := time.Now() + ctx, span := cfg.tracer.Start( + ctx, + cfg.clientSpanNameFormatter(req), + oteltrace.WithTimestamp(start), + oteltrace.WithSpanKind(oteltrace.SpanKindClient), + ) + defer span.End() + var labels []label.CwLabel + // inject client service resource attributes (canonical service) to meta map + md := injectPeerServiceToMetadata(ctx, span.(trace.ReadOnlySpan).Resource().Attributes()) + + Inject(ctx, cfg, &req.Header) + + for k, v := range md { + req.Header.Set(k, v) + } + + err = next(ctx, req, resp) + + // end span + if httpReq, err := adaptor.GetCompatRequest(req); err == nil { + span.SetAttributes(semconv.NetAttributesFromHTTPRequest("tcp", httpReq)...) + span.SetAttributes(semconv.EndUserAttributesFromHTTPRequest(httpReq)...) + span.SetAttributes(semconv.HTTPServerAttributesFromHTTPRequest("", cfg.clientSpanNameFormatter(req), httpReq)...) + } + + // span attributes + attrs := []attribute.KeyValue{ + semconv.HTTPURLKey.String(req.URI().String()), + } + + if err == nil { + // set span status with resp status code + span.SetStatus(semconv.SpanStatusFromHTTPStatusCode(resp.StatusCode())) + labels = append(labels, label.CwLabel{ + Key: semantic.LabelStatusCode, + Value: strconv.Itoa(resp.StatusCode()), + }) + + } else { + // resp.StatusCode() is not valid when client returns error + span.SetStatus(codes.Error, err.Error()) + } + span.SetAttributes(attrs...) + + // extract meter attr + metricsAttributes := semantic.ExtractMetricsAttributesFromSpan(span) + + // record meter + labels = append(labels, label.ToCwLabelsFromOtels(metricsAttributes)...) + cfg.measure.Inc(ctx, semantic.HTTPCounter, labels...) + cfg.measure.Record(ctx, semantic.HTTPLatency, float64(time.Since(start))/float64(time.Millisecond), labels...) + return + } + } +} + +func ServerMiddleware(cfg *Config) app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + if cfg.shouldIgnore(ctx, c) { + c.Next(ctx) + return + } + // get tracer carrier + tc := internal.TraceCarrierFromContext(ctx) + if tc == nil { + hlog.CtxWarnf(ctx, "TraceCarrier not found in context") + c.Next(ctx) + return + } + + sTracer := tc.Tracer() + ti := c.GetTraceInfo() + if ti.Stats().Level() == stats.LevelDisabled { + c.Next(ctx) + return + } + + opts := []oteltrace.SpanStartOption{ + oteltrace.WithTimestamp(getStartTimeOrNow(ti)), + oteltrace.WithSpanKind(oteltrace.SpanKindServer), + } + + peerServiceAttributes := extractPeerServiceAttributesFromMetadata(&c.Request.Header) + + // extract baggage and span context from header + bags, spanCtx := Extract(ctx, cfg, &c.Request.Header) + + // set baggage + ctx = baggage.ContextWithBaggage(ctx, bags) + + ctx, span := sTracer.Start(oteltrace.ContextWithRemoteSpanContext(ctx, spanCtx), cfg.serverSpanNameFormatter(c), opts...) + + // peer service attributes + span.SetAttributes(peerServiceAttributes...) + + // set span and attrs into tracer carrier for serverTracer finish + tc.SetSpan(span) + + c.Next(ctx) + + if cfg.customResponseHandler != nil { + // execute custom response handler + cfg.customResponseHandler(ctx, c) + } + } +} diff --git a/telemetry/instrumentation/otelhertz/middleware_test.go b/telemetry/instrumentation/otelhertz/middleware_test.go new file mode 100644 index 0000000..430e32a --- /dev/null +++ b/telemetry/instrumentation/otelhertz/middleware_test.go @@ -0,0 +1,70 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/test/assert" + "github.com/cloudwego/hertz/pkg/common/tracer/stats" + "go.opentelemetry.io/otel" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + oteltrace "go.opentelemetry.io/otel/trace" +) + +func TestServerMiddleware(t *testing.T) { + sr := tracetest.NewSpanRecorder() + otel.SetTracerProvider(sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))) + tracer, cfg := NewServerOption(WithCustomResponseHandler(func(c context.Context, ctx *app.RequestContext) { + ctx.Header("trace-id", oteltrace.SpanFromContext(c).SpanContext().TraceID().String()) + })) + h := server.Default(tracer, server.WithHostPorts("127.0.0.1:6666")) + h.Use(ServerMiddleware(cfg)) + h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { + }) + + go h.Spin() + time.Sleep(100 * time.Millisecond) + resp, err := http.Get("http://127.0.0.1:6666/ping") + assert.Nil(t, err) + assert.True(t, len(resp.Header.Get("trace-id")) != 0) +} + +func TestServerMiddlewareDisableTrace(t *testing.T) { + sr := tracetest.NewSpanRecorder() + otel.SetTracerProvider(sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))) + tracer, cfg := NewServerOption(WithCustomResponseHandler(func(c context.Context, ctx *app.RequestContext) { + ctx.Header("trace-id", oteltrace.SpanFromContext(c).SpanContext().TraceID().String()) + })) + h := server.Default(tracer, + server.WithHostPorts("127.0.0.1:16666"), + server.WithTraceLevel(stats.LevelDisabled), + ) + h.Use(ServerMiddleware(cfg)) + h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { + }) + + go h.Spin() + time.Sleep(100 * time.Millisecond) + resp, err := http.Get("http://127.0.0.1:16666/ping") + assert.Nil(t, err) + assert.True(t, len(resp.Header.Get("trace-id")) == 0) +} diff --git a/telemetry/instrumentation/otelhertz/options.go b/telemetry/instrumentation/otelhertz/options.go new file mode 100644 index 0000000..75d981f --- /dev/null +++ b/telemetry/instrumentation/otelhertz/options.go @@ -0,0 +1,196 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "context" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/global" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + cwmetric "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/protocol" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +const ( + instrumentationName = "github.com/cloudwego-contrib/telemetry-opentelemetry" +) + +// Option opts for opentelemetry tracer provider +type Option interface { + apply(cfg *Config) +} + +type option func(cfg *Config) + +func (fn option) apply(cfg *Config) { + fn(cfg) +} + +type ConditionFunc func(ctx context.Context, c *app.RequestContext) bool + +type Config struct { + tracer trace.Tracer + + clientHttpRouteFormatter func(req *protocol.Request) string + serverHttpRouteFormatter func(c *app.RequestContext) string + + clientSpanNameFormatter func(req *protocol.Request) string + serverSpanNameFormatter func(c *app.RequestContext) string + + labelFunc func(c *app.RequestContext) []label.CwLabel + + tracerProvider trace.TracerProvider + textMapPropagator propagation.TextMapPropagator + + recordSourceOperation bool + + customResponseHandler app.HandlerFunc + shouldIgnore ConditionFunc + measure cwmetric.Measure +} + +func NewConfig(opts ...Option) *Config { + cfg := DefaultConfig() + + for _, opt := range opts { + opt.apply(cfg) + } + + cfg.tracer = cfg.tracerProvider.Tracer( + instrumentationName, + trace.WithInstrumentationVersion(semantic.SemVersion()), + ) + + return cfg +} + +func DefaultConfig() *Config { + return &Config{ + tracerProvider: otel.GetTracerProvider(), + textMapPropagator: otel.GetTextMapPropagator(), + customResponseHandler: func(c context.Context, ctx *app.RequestContext) {}, + clientHttpRouteFormatter: func(req *protocol.Request) string { + return string(req.Path()) + }, + clientSpanNameFormatter: func(req *protocol.Request) string { + return string(req.Method()) + " " + string(req.Path()) + }, + serverHttpRouteFormatter: func(c *app.RequestContext) string { + // FullPath returns a matched route full path. For not found routes + // returns an empty string. + route := c.FullPath() + // fall back to path + if route == "" { + route = string(c.Path()) + } + return route + }, + serverSpanNameFormatter: func(c *app.RequestContext) string { + // Ref to https://github.com/open-telemetry/opentelemetry-specification/blob/ffddc289462dfe0c2041e3ca42a7b1df805706de/specification/trace/api.md#span + // FullPath returns a matched route full path. For not found routes + // returns an empty string. + route := c.FullPath() + // fall back to handler name + if route == "" { + route = string(c.Path()) + } + return string(c.Method()) + " " + route + }, + shouldIgnore: func(ctx context.Context, c *app.RequestContext) bool { + return false + }, + measure: global.GetTracerMeasure(), + } +} + +func (c *Config) GetTextMapPropagator() propagation.TextMapPropagator { + return c.textMapPropagator +} + +// WithRecordSourceOperation configures record source operation dimension +func WithRecordSourceOperation(recordSourceOperation bool) Option { + return option(func(cfg *Config) { + cfg.recordSourceOperation = recordSourceOperation + }) +} + +// WithTextMapPropagator configures propagation +func WithTextMapPropagator(p propagation.TextMapPropagator) Option { + return option(func(cfg *Config) { + cfg.textMapPropagator = p + }) +} + +// WithCustomResponseHandler configures CustomResponseHandler +func WithCustomResponseHandler(h app.HandlerFunc) Option { + return option(func(cfg *Config) { + cfg.customResponseHandler = h + }) +} + +// WithClientHttpRouteFormatter configures clientHttpRouteFormatter +func WithClientHttpRouteFormatter(clientHttpRouteFormatter func(req *protocol.Request) string) Option { + return option(func(cfg *Config) { + cfg.clientHttpRouteFormatter = clientHttpRouteFormatter + }) +} + +// WithServerHttpRouteFormatter configures serverHttpRouteFormatter +func WithServerHttpRouteFormatter(serverHttpRouteFormatter func(c *app.RequestContext) string) Option { + return option(func(cfg *Config) { + cfg.serverHttpRouteFormatter = serverHttpRouteFormatter + }) +} + +// WithClientSpanNameFormatter configures clientSpanNameFormatter +func WithClientSpanNameFormatter(clientSpanNameFormatter func(req *protocol.Request) string) Option { + return option(func(cfg *Config) { + cfg.clientSpanNameFormatter = clientSpanNameFormatter + }) +} + +// WithServerSpanNameFormatter configures serverSpanNameFormatter +func WithServerSpanNameFormatter(serverSpanNameFormatter func(c *app.RequestContext) string) Option { + return option(func(cfg *Config) { + cfg.serverSpanNameFormatter = serverSpanNameFormatter + }) +} + +// WithShouldIgnore allows you to define the condition for enabling distributed semantic +func WithShouldIgnore(condition ConditionFunc) Option { + return option(func(cfg *Config) { + cfg.shouldIgnore = condition + }) +} + +// WithMeasure define your measure +func WithMeasure(measure cwmetric.Measure) Option { + return option(func(cfg *Config) { + cfg.measure = measure + }) +} + +func WithLabelFunc(getLabelFromRequest func(c *app.RequestContext) []label.CwLabel) Option { + return option(func(cfg *Config) { + cfg.labelFunc = getLabelFromRequest + }) +} diff --git a/telemetry/instrumentation/otelhertz/peer.go b/telemetry/instrumentation/otelhertz/peer.go new file mode 100644 index 0000000..16f909e --- /dev/null +++ b/telemetry/instrumentation/otelhertz/peer.go @@ -0,0 +1,72 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "context" + "strings" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/cloudwego/hertz/pkg/protocol" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.10.0" +) + +func injectPeerServiceToMetadata(_ context.Context, attrs []attribute.KeyValue) map[string]string { + serviceName, serviceNamespace, deploymentEnv := getServiceFromResourceAttributes(attrs) + + md := make(map[string]string, 3) + + if serviceName != "" { + md[semconvAttributeKeyToHTTPHeader(string(semconv.ServiceNameKey))] = serviceName + } + + if serviceNamespace != "" { + md[semconvAttributeKeyToHTTPHeader(string(semconv.ServiceNamespaceKey))] = serviceNamespace + } + + if deploymentEnv != "" { + md[semconvAttributeKeyToHTTPHeader(string(semconv.DeploymentEnvironmentKey))] = deploymentEnv + } + + return md +} + +func extractPeerServiceAttributesFromMetadata(headers *protocol.RequestHeader) []attribute.KeyValue { + var attrs []attribute.KeyValue + + serviceName, serviceNamespace, deploymentEnv := headers.Get(semconvAttributeKeyToHTTPHeader(string(semconv.ServiceNameKey))), + headers.Get(semconvAttributeKeyToHTTPHeader(string(semconv.ServiceNamespaceKey))), + headers.Get(semconvAttributeKeyToHTTPHeader(string(semconv.DeploymentEnvironmentKey))) + + if serviceName != "" { + attrs = append(attrs, semconv.PeerServiceKey.String(serviceName)) + } + + if serviceNamespace != "" { + attrs = append(attrs, semantic.PeerServiceNamespaceKey.String(serviceNamespace)) + } + + if deploymentEnv != "" { + attrs = append(attrs, semantic.PeerDeploymentEnvironmentKey.String(deploymentEnv)) + } + + return attrs +} + +func semconvAttributeKeyToHTTPHeader(key string) string { + return strings.ReplaceAll(key, ".", "-") +} diff --git a/telemetry/instrumentation/otelhertz/propagator.go b/telemetry/instrumentation/otelhertz/propagator.go new file mode 100644 index 0000000..7a87cb0 --- /dev/null +++ b/telemetry/instrumentation/otelhertz/propagator.go @@ -0,0 +1,63 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "context" + + "github.com/cloudwego/hertz/pkg/protocol" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +var _ propagation.TextMapCarrier = &metadataProvider{} + +type metadataProvider struct { + metadata map[string]string + headers *protocol.RequestHeader +} + +// Get a value from metadata by key +func (m *metadataProvider) Get(key string) string { + return m.headers.Get(key) +} + +// Set a value to metadata by k/v +func (m *metadataProvider) Set(key, value string) { + m.headers.Set(key, value) +} + +// Keys Iteratively get all keys of metadata +func (m *metadataProvider) Keys() []string { + out := make([]string, 0, len(m.metadata)) + + m.headers.VisitAll(func(key, value []byte) { + out = append(out, string(key)) + }) + + return out +} + +// Inject injects span context into the otelhertz metadata info +func Inject(ctx context.Context, c *Config, headers *protocol.RequestHeader) { + c.GetTextMapPropagator().Inject(ctx, &metadataProvider{headers: headers}) +} + +// Extract returns the baggage and span context +func Extract(ctx context.Context, c *Config, headers *protocol.RequestHeader) (baggage.Baggage, trace.SpanContext) { + ctx = c.GetTextMapPropagator().Extract(ctx, &metadataProvider{headers: headers}) + return baggage.FromContext(ctx), trace.SpanContextFromContext(ctx) +} diff --git a/telemetry/instrumentation/otelhertz/propagator_test.go b/telemetry/instrumentation/otelhertz/propagator_test.go new file mode 100644 index 0000000..171c461 --- /dev/null +++ b/telemetry/instrumentation/otelhertz/propagator_test.go @@ -0,0 +1,124 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "context" + "reflect" + "testing" + + "github.com/bytedance/gopkg/cloud/metainfo" + + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/contrib/propagators/ot" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +func TestExtract(t *testing.T) { + ctx := context.Background() + bags, _ := baggage.Parse("foo=bar") + ctx = baggage.ContextWithBaggage(ctx, bags) + ctx = metainfo.WithValue(ctx, "foo", "bar") + + headers := &protocol.RequestHeader{} + headers.Set("foo", "bar") + + type args struct { + ctx context.Context + c *Config + metadata *protocol.RequestHeader + } + tests := []struct { + name string + args args + want baggage.Baggage + want1 trace.SpanContext + }{ + { + name: "extract successful", + args: args{ + ctx: ctx, + c: DefaultConfig(), + metadata: headers, + }, + want: bags, + want1: trace.SpanContext{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := Extract(tt.args.ctx, tt.args.c, tt.args.metadata) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Extract() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("Extract() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestInject(t *testing.T) { + cfg := NewConfig([]Option{WithTextMapPropagator(propagation.NewCompositeTextMapPropagator( + b3.New(), + ot.OT{}, + propagation.Baggage{}, + propagation.TraceContext{}, + ))}...) + + ctx := context.Background() + + spanContext := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: [16]byte{1}, + SpanID: [8]byte{2}, + TraceFlags: 0, + TraceState: trace.TraceState{}, + Remote: false, + }) + + ctx = trace.ContextWithSpanContext(ctx, spanContext) + md := &protocol.RequestHeader{} + + type args struct { + ctx context.Context + c *Config + metadata *protocol.RequestHeader + } + tests := []struct { + name string + args args + }{ + { + name: "inject valid", + args: args{ + ctx: ctx, + c: cfg, + metadata: md, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Inject(tt.args.ctx, tt.args.c, tt.args.metadata) + assert.NotEmpty(t, tt.args.metadata) + assert.Equal(t, "01000000000000000000000000000000-0200000000000000-0", md.Get("b3")) + assert.Equal(t, "00-01000000000000000000000000000000-0200000000000000-00", md.Get("traceparent")) + }) + } +} diff --git a/telemetry/instrumentation/otelhertz/testdata/hertz_request_metrics.txt b/telemetry/instrumentation/otelhertz/testdata/hertz_request_metrics.txt new file mode 100644 index 0000000..5e64b37 --- /dev/null +++ b/telemetry/instrumentation/otelhertz/testdata/hertz_request_metrics.txt @@ -0,0 +1,7 @@ +# HELP http_client_request_count_total measures the client request count total +# TYPE http_client_request_count_total counter +http_client_request_count_total{deployment_environment="test-env",http_host="localhost:39887",http_method="GET",http_route="GET /ping",net_transport="ip_tcp",otel_scope_name="github.com/cloudwego-contrib/telemetry-opentelemetry",otel_scope_version="semver:0.39.0",service_name="test-server",service_namespace="test-ns",status_code="Error"} 1 +http_client_request_count_total{deployment_environment="test-env",http_host="localhost:39888",http_method="GET",http_route="GET /ping",http_status_code="200",net_transport="ip_tcp",otel_scope_name="github.com/cloudwego-contrib/telemetry-opentelemetry",otel_scope_version="semver:0.39.0",service_name="test-server",service_namespace="test-ns",status_code="Unset"} 1 +# HELP http_server_request_count_total measures Incoming request count total +# TYPE http_server_request_count_total counter +http_server_request_count_total{deployment_environment="test-env",http_host="localhost:39888",http_method="GET",http_route="/ping",http_status_code="200",net_transport="ip_tcp",otel_scope_name="github.com/cloudwego-contrib/telemetry-opentelemetry",otel_scope_version="semver:0.39.0",path="/ping",peer_deployment_environment="test-env",peer_service="test-server",peer_service_namespace="test-ns",service_name="test-server",service_namespace="test-ns",status_code="Unset"} 1 diff --git a/telemetry/instrumentation/otelhertz/testutil/otel.go b/telemetry/instrumentation/otelhertz/testutil/otel.go new file mode 100644 index 0000000..2b15b4c --- /dev/null +++ b/telemetry/instrumentation/otelhertz/testutil/otel.go @@ -0,0 +1,163 @@ +// Copyright 2022 CloudWeGo 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. + +package testutil + +import ( + "os" + + cwmetric "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + + otelprom "go.opentelemetry.io/otel/exporters/prometheus" + stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + otelmetric "go.opentelemetry.io/otel/metric" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +// OtelTestProvider get otel test provider +func OtelTestProvider() (*sdktrace.TracerProvider, *prometheus.Registry, cwmetric.Measure, cwmetric.Measure) { + // prometheus registry + registry := prometheus.NewRegistry() + + // init tracer + tracerProvider, err := initTracer() + if err != nil { + panic(err) + } + + meterProvider, err := initMeterProvider(registry) + if err != nil { + panic(err) + } + // measure for client + meter := meterProvider.Meter( + "github.com/cloudwego-contrib/telemetry-opentelemetry", + otelmetric.WithInstrumentationVersion(semantic.SemVersion()), + ) + clientRequestCountMeasure, err := meter.Int64Counter( + semantic.BuildMetricName("http", "client", semantic.RequestCount), + otelmetric.WithUnit("count"), + otelmetric.WithDescription("measures the client request count total"), + ) + HandleErr(err) + + clientLatencyMeasure, err := meter.Float64Histogram( + semantic.BuildMetricName("http", "client", semantic.ServerLatency), + otelmetric.WithUnit("ms"), + otelmetric.WithDescription("measures the duration outbound HTTP requests"), + ) + HandleErr(err) + + measureClient := cwmetric.NewMeasure( + cwmetric.WithCounter(semantic.HTTPCounter, cwmetric.NewOtelCounter(clientRequestCountMeasure)), + cwmetric.WithRecorder(semantic.HTTPLatency, cwmetric.NewOtelRecorder(clientLatencyMeasure)), + ) + // Measure for server + meter = meterProvider.Meter( + "github.com/cloudwego-contrib/telemetry-opentelemetry", + otelmetric.WithInstrumentationVersion(semantic.SemVersion()), + ) + serverRequestCountMeasure, err := meter.Int64Counter( + semantic.BuildMetricName("http", "server", semantic.RequestCount), + otelmetric.WithUnit("count"), + otelmetric.WithDescription("measures Incoming request count total"), + ) + HandleErr(err) + + serverLatencyMeasure, err := meter.Float64Histogram( + semantic.BuildMetricName("http", "server", semantic.ServerLatency), + otelmetric.WithUnit("ms"), + otelmetric.WithDescription("measures th incoming end to end duration"), + ) + HandleErr(err) + + measureServer := cwmetric.NewMeasure( + cwmetric.WithCounter(semantic.HTTPCounter, cwmetric.NewOtelCounter(serverRequestCountMeasure)), + cwmetric.WithRecorder(semantic.HTTPLatency, cwmetric.NewOtelRecorder(serverLatencyMeasure)), + ) + return tracerProvider, registry, measureClient, measureServer +} + +// GatherAndCompare compare meter with registry +func GatherAndCompare(registry *prometheus.Registry, expectedFilePath string, metricName ...string) error { + file, err := os.Open(expectedFilePath) + if err != nil { + return err + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + err = testutil.GatherAndCompare(registry, file, metricName...) + if err != nil { + return err + } + return nil +} + +func initMeterProvider(registry *prometheus.Registry) (otelmetric.MeterProvider, error) { + exporter, err := initMetricExporter(registry) + if err != nil { + return nil, err + } + provider := metric.NewMeterProvider(metric.WithReader(exporter)) + return provider, nil +} + +func initMetricExporter(registry *prometheus.Registry) (*otelprom.Exporter, error) { + return otelprom.New( + otelprom.WithRegisterer(registry), + ) +} + +func initTracer() (*sdktrace.TracerProvider, error) { + // Create stdout exporter to be able to retrieve + // the collected spans. + exporter, err := stdout.New(stdout.WithPrettyPrint()) + if err != nil { + return nil, err + } + + // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. + // In a production application, use sdktrace.ProbabilitySampler with a desired probability. + tp := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("test-server"), + semconv.ServiceNamespaceKey.String("test-ns"), + semconv.DeploymentEnvironmentKey.String("test-env"), + )), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + return tp, err +} + +func HandleErr(err error) { + if err != nil { + otel.Handle(err) + } +} diff --git a/telemetry/instrumentation/otelhertz/trace.go b/telemetry/instrumentation/otelhertz/trace.go new file mode 100644 index 0000000..dac3ea4 --- /dev/null +++ b/telemetry/instrumentation/otelhertz/trace.go @@ -0,0 +1,27 @@ +/* + * Copyright 2022 CloudWeGo 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. + */ + +package otelhertz + +// NewServerTracer provides tracer for server access, addr and path is the scrape_configs for prometheus server. +func NewServerTracer(opts ...Option) *HertzTracer { + cfg := NewConfig(opts...) + + return &HertzTracer{ + measure: cfg.measure, + cfg: cfg, + } +} diff --git a/telemetry/instrumentation/otelhertz/tracer_server.go b/telemetry/instrumentation/otelhertz/tracer_server.go new file mode 100644 index 0000000..bbce2ea --- /dev/null +++ b/telemetry/instrumentation/otelhertz/tracer_server.go @@ -0,0 +1,26 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "github.com/cloudwego/hertz/pkg/app/server" + serverconfig "github.com/cloudwego/hertz/pkg/common/config" +) + +func NewServerOption(opts ...Option) (serverconfig.Option, *Config) { + hertzTracer := NewServerTracer(opts...) + + return server.WithTracer(hertzTracer), hertzTracer.cfg +} diff --git a/telemetry/instrumentation/otelhertz/utils.go b/telemetry/instrumentation/otelhertz/utils.go new file mode 100644 index 0000000..05ad42d --- /dev/null +++ b/telemetry/instrumentation/otelhertz/utils.go @@ -0,0 +1,93 @@ +// Copyright 2022 CloudWeGo 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. + +package otelhertz + +import ( + "errors" + "fmt" + "time" + + "github.com/cloudwego/hertz/pkg/common/tracer/stats" + "github.com/cloudwego/hertz/pkg/common/tracer/traceinfo" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.10.0" + "go.opentelemetry.io/otel/trace" +) + +func getStartTimeOrNow(ti traceinfo.TraceInfo) time.Time { + if event := ti.Stats().GetEvent(stats.HTTPStart); event != nil { + return event.Time() + } + return time.Now() +} + +func getEndTimeOrNow(ti traceinfo.TraceInfo) time.Time { + if event := ti.Stats().GetEvent(stats.HTTPFinish); event != nil { + return event.Time() + } + return time.Now() +} + +func getServiceFromResourceAttributes(attrs []attribute.KeyValue) (serviceName, serviceNamespace, deploymentEnv string) { + for _, attr := range attrs { + switch attr.Key { + case semconv.ServiceNameKey: + serviceName = attr.Value.AsString() + case semconv.ServiceNamespaceKey: + serviceNamespace = attr.Value.AsString() + case semconv.DeploymentEnvironmentKey: + deploymentEnv = attr.Value.AsString() + } + } + return +} + +func parseHTTPError(ri traceinfo.TraceInfo) (panicMsg, panicStack string, err error) { + panicked, panicErr := ri.Stats().Panicked() + if err = ri.Stats().Error(); err == nil && !panicked { + return + } + if panicked { + panicMsg = fmt.Sprintf("%v", panicErr) + if stackErr, ok := panicErr.(interface{ Stack() string }); ok { + panicStack = stackErr.Stack() + } + } + return +} + +// recordErrorSpanWithStack record error with stack +func recordErrorSpanWithStack(span trace.Span, err error, stackMessage, stackTrace string, attributes ...attribute.KeyValue) { + if span == nil { + return + } + + // compatible with the case where error is empty + if err == nil { + err = errors.New(stackMessage) + } + + // stack trace + attributes = append(attributes, + semconv.ExceptionStacktraceKey.String(stackTrace), + ) + + span.SetStatus(codes.Error, err.Error()) + span.RecordError( + err, + trace.WithAttributes(attributes...), + ) +} diff --git a/telemetry/instrumentation/otelkitex/doc.go b/telemetry/instrumentation/otelkitex/doc.go new file mode 100644 index 0000000..600aa69 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/doc.go @@ -0,0 +1,16 @@ +// Copyright 2022 CloudWeGo 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. + +// Package otelkitex provides the otel otelkitex & meter implement of tracer +package otelkitex diff --git a/telemetry/instrumentation/otelkitex/events.go b/telemetry/instrumentation/otelkitex/events.go new file mode 100644 index 0000000..a3bc91b --- /dev/null +++ b/telemetry/instrumentation/otelkitex/events.go @@ -0,0 +1,51 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + "github.com/cloudwego/kitex/pkg/rpcinfo" + "github.com/cloudwego/kitex/pkg/stats" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var commonEvents = map[string]stats.Event{ + "server_handle_start": stats.ServerHandleStart, + "server_handle_finish": stats.ServerHandleFinish, + "client_conn_start": stats.ClientConnStart, + "client_conn_finish": stats.ClientConnFinish, + "read_start": stats.ReadStart, + "read_finish": stats.ReadFinish, + "wait_read_start": stats.WaitReadStart, + "wait_read_finish": stats.WaitReadFinish, + "write_start": stats.WriteStart, + "write_finish": stats.WriteFinish, +} + +func injectStatsEventsToSpan(span trace.Span, st rpcinfo.RPCStats) { + for name, event := range commonEvents { + if gotEvent := st.GetEvent(event); gotEvent != nil { + attrs := []attribute.KeyValue{attribute.Int(semantic.LabelKeyStatus, int(gotEvent.Status()))} + if gotEvent.Info() != "" { + attrs = append(attrs, attribute.String("event.info", gotEvent.Info())) + } + span.AddEvent(name, + trace.WithTimestamp(gotEvent.Time()), + trace.WithAttributes(attrs...), + ) + } + } +} diff --git a/telemetry/instrumentation/otelkitex/kitex_tracer.go b/telemetry/instrumentation/otelkitex/kitex_tracer.go new file mode 100644 index 0000000..f0dcce7 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/kitex_tracer.go @@ -0,0 +1,150 @@ +// 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 otelkitex + +import ( + "context" + "strconv" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/internal" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + cwmetric "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" + "github.com/cloudwego/kitex/pkg/rpcinfo" + "github.com/cloudwego/kitex/pkg/stats" +) + +var _ stats.Tracer = (*KitexTracer)(nil) + +type KitexTracer struct { + measure cwmetric.Measure + cfg *Config + recordSourceOperation bool +} + +// Start record the beginning of an RPC invocation. +func (s *KitexTracer) Start(ctx context.Context) context.Context { + tc := &internal.TraceCarrier{} + if s.cfg.tracer != nil { + tc.SetTracer(s.cfg.tracer) + } + + return internal.WithTraceCarrier(ctx, tc) +} + +// Finish record after receiving the response of server. +func (s *KitexTracer) Finish(ctx context.Context) { + // rpc info + ri := rpcinfo.GetRPCInfo(ctx) + if ri.Stats().Level() == stats.LevelDisabled { + return + } + + st := ri.Stats() + rpcStart := st.GetEvent(stats.RPCStart) + rpcFinish := st.GetEvent(stats.RPCFinish) + duration := rpcFinish.Time().Sub(rpcStart.Time()) + elapsedTime := float64(duration) / float64(time.Millisecond) + + caller := ri.From() + callee := ri.To() + labels := []label.CwLabel{ + { + Key: semantic.LabelRPCCallerKey, + Value: defaultValIfEmpty(caller.ServiceName(), semantic.UnknownLabelValue), + }, + { + Key: semantic.LabelRPCCalleeKey, + Value: defaultValIfEmpty(callee.ServiceName(), semantic.UnknownLabelValue), + }, + { + Key: semantic.LabelRPCMethodKey, + Value: defaultValIfEmpty(callee.Method(), semantic.UnknownLabelValue), + }, + } + + if retriedCnt, ok := callee.Tag(rpcinfo.RetryTag); ok { + retryAttempts, err := strconv.Atoi(retriedCnt) + if err == nil { + s.measure.Record(ctx, semantic.RPCRetry, float64(retryAttempts), labels...) + } + + } + + if s.cfg.labelFunc != nil { + labels = append(labels, s.cfg.labelFunc(ri)...) + } + + tc := internal.TraceCarrierFromContext(ctx) + + // span + var span trace.Span + if tc != nil && tc.Span() != nil && tc.Span().IsRecording() { + span = tc.Span() + // span attributes + attrs := []attribute.KeyValue{ + semantic.RPCSystemKitex, + semantic.RPCSystemKitexRecvSize.Int64(int64(st.RecvSize())), + semantic.RPCSystemKitexSendSize.Int64(int64(st.SendSize())), + semantic.RequestProtocolKey.String(ri.Config().TransportProtocol().String()), + } + + // The source operation dimension maybe cause high cardinality issues + if s.recordSourceOperation { + attrs = append(attrs, semantic.SourceOperationKey.String(ri.From().Method())) + } + + span.SetAttributes(attrs...) + + injectStatsEventsToSpan(span, st) + + if panicMsg, panicStack, rpcErr := parseRPCError(ri); rpcErr != nil || len(panicMsg) > 0 { + recordErrorSpanWithStack(span, rpcErr, panicMsg, panicStack) + } + + span.End(trace.WithTimestamp(getEndTimeOrNow(ri))) + metricsAttributes := semantic.ExtractMetricsAttributesFromSpan(span) + spanlabels := label.ToCwLabelsFromOtels(metricsAttributes) + + labels = append(labels, spanlabels...) + + } + if span == nil || !span.IsRecording() { + stateless := label.CwLabel{ + Key: semantic.LabelKeyStatus, + Value: semantic.StatusSucceed, + } + if ri.Stats().Error() != nil { + stateless = label.CwLabel{ + Key: semantic.LabelKeyStatus, + Value: semantic.StatusError, + } + } + labels = append(labels, stateless) + } + + // measure + s.measure.Inc(ctx, semantic.RPCCounter, labels...) + s.measure.Record(ctx, semantic.RPCLatency, elapsedTime, labels...) +} + +func defaultValIfEmpty(val, def string) string { + if val == "" { + return def + } + return val +} diff --git a/telemetry/instrumentation/otelkitex/metadata_supplier.go b/telemetry/instrumentation/otelkitex/metadata_supplier.go new file mode 100644 index 0000000..19e2bee --- /dev/null +++ b/telemetry/instrumentation/otelkitex/metadata_supplier.go @@ -0,0 +1,64 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package otelkitex + +import ( + "context" + + "github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/metadata" + "go.opentelemetry.io/otel/propagation" +) + +type metadataSupplier struct { + metadata *metadata.MD +} + +// assert that metadataSupplier implements the TextMapCarrier interface. +var _ propagation.TextMapCarrier = &metadataSupplier{} + +func (s *metadataSupplier) Get(key string) string { + values := s.metadata.Get(key) + if len(values) == 0 { + return "" + } + return values[0] +} + +func (s *metadataSupplier) Set(key string, value string) { + s.metadata.Set(key, value) +} + +func (s *metadataSupplier) Keys() []string { + out := make([]string, 0, len(*s.metadata)) + for key := range *s.metadata { + out = append(out, key) + } + return out +} + +func injectMetadata(ctx context.Context, cfg *Config, md metadata.MD) context.Context { + cfg.textMapPropagator.Inject(ctx, &metadataSupplier{ + metadata: &md, + }) + return metadata.NewOutgoingContext(ctx, md) +} + +func extractMetadata(ctx context.Context, cfg *Config, md metadata.MD) context.Context { + return cfg.textMapPropagator.Extract(ctx, &metadataSupplier{ + metadata: &md, + }) +} diff --git a/telemetry/instrumentation/otelkitex/middleware.go b/telemetry/instrumentation/otelkitex/middleware.go new file mode 100644 index 0000000..de0d22b --- /dev/null +++ b/telemetry/instrumentation/otelkitex/middleware.go @@ -0,0 +1,110 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "context" + + "github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/metadata" + + "github.com/cloudwego/kitex/pkg/klog" + + "github.com/bytedance/gopkg/cloud/metainfo" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/internal" + "github.com/cloudwego/kitex/pkg/endpoint" + "github.com/cloudwego/kitex/pkg/rpcinfo" + + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/sdk/trace" + oteltrace "go.opentelemetry.io/otel/trace" +) + +// ClientMiddleware inject span context into req meta +func ClientMiddleware(cfg *Config) endpoint.Middleware { + return func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req, resp interface{}) (err error) { + span := oteltrace.SpanFromContext(ctx) + if !span.IsRecording() { + return next(ctx, req, resp) + } + + readOnlySpan := span.(trace.ReadOnlySpan) + + // inject client service resource attributes (canonical service) to meta info + md := injectPeerServiceToMetaInfo(ctx, readOnlySpan.Resource().Attributes()) + + Inject(ctx, cfg, md) + + if cfg.enableGRPCMetadata { + grpcMd, ok := metadata.FromOutgoingContext(ctx) + if ok { + ctx = injectMetadata(ctx, cfg, grpcMd) + } + } + + for k, v := range md { + ctx = metainfo.WithValue(ctx, k, v) + } + + return next(ctx, req, resp) + } + } +} + +// ServerMiddleware extract req meta into span context +func ServerMiddleware(cfg *Config) endpoint.Middleware { + return func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req, resp interface{}) (err error) { + tc := internal.TraceCarrierFromContext(ctx) + if tc == nil { + klog.CtxWarnf(ctx, "TraceCarrier not found in context") + return next(ctx, req, resp) + } + + // get tracer from carrier + sTracer := tc.Tracer() + + ri := rpcinfo.GetRPCInfo(ctx) + opts := []oteltrace.SpanStartOption{ + oteltrace.WithTimestamp(getStartTimeOrNow(ri)), + oteltrace.WithSpanKind(oteltrace.SpanKindServer), + } + + md := metainfo.GetAllValues(ctx) + peerServiceAttributes := extractPeerServiceAttributesFromMetaInfo(md) + + if cfg.enableGRPCMetadata { + grpcMd, ok := metadata.FromOutgoingContext(ctx) + if ok { + ctx = injectMetadata(ctx, cfg, grpcMd) + } + } + + bags, spanCtx := Extract(ctx, cfg, md) + ctx = baggage.ContextWithBaggage(ctx, bags) + + ctx, span := sTracer.Start(oteltrace.ContextWithRemoteSpanContext(ctx, spanCtx), spanNaming(ri), opts...) + + // peer service attributes + span.SetAttributes(peerServiceAttributes...) + + // set span and attrs into tracer carrier for serverTracer finish + tc.SetSpan(span) + + return next(ctx, req, resp) + } + } +} diff --git a/telemetry/instrumentation/otelkitex/options.go b/telemetry/instrumentation/otelkitex/options.go new file mode 100644 index 0000000..560db37 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/options.go @@ -0,0 +1,116 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/global" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + cwmetric "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + "github.com/cloudwego/kitex/pkg/rpcinfo" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +const ( + instrumentationName = "github.com/otelkitex-contrib/telemetry-opentelemetry" +) + +// Option opts for opentelemetry tracer provider +type Option interface { + apply(cfg *Config) +} + +type option func(cfg *Config) + +func (fn option) apply(cfg *Config) { + fn(cfg) +} + +type Config struct { + tracer trace.Tracer + + labelFunc func(info rpcinfo.RPCInfo) []label.CwLabel + tracerProvider trace.TracerProvider + meterProvider metric.MeterProvider + textMapPropagator propagation.TextMapPropagator + + recordSourceOperation bool + enableGRPCMetadata bool + + measure cwmetric.Measure +} + +func NewConfig(opts []Option) *Config { + cfg := DefaultConfig() + for _, opt := range opts { + opt.apply(cfg) + } + + cfg.tracer = cfg.tracerProvider.Tracer( + instrumentationName, + trace.WithInstrumentationVersion(semantic.SemVersion()), + ) + + return cfg +} + +func DefaultConfig() *Config { + return &Config{ + tracerProvider: otel.GetTracerProvider(), + meterProvider: otel.GetMeterProvider(), + textMapPropagator: otel.GetTextMapPropagator(), + measure: global.GetTracerMeasure(), + } +} + +func (c Config) GetTextMapPropagator() propagation.TextMapPropagator { + return c.textMapPropagator +} + +// WithRecordSourceOperation configures record source operation dimension +func WithRecordSourceOperation(recordSourceOperation bool) Option { + return option(func(cfg *Config) { + cfg.recordSourceOperation = recordSourceOperation + }) +} + +// WithTextMapPropagator configures propagation +func WithTextMapPropagator(p propagation.TextMapPropagator) Option { + return option(func(cfg *Config) { + cfg.textMapPropagator = p + }) +} + +// WithMeasure define your custom measure +func WithMeasure(measure cwmetric.Measure) Option { + return option(func(cfg *Config) { + cfg.measure = measure + }) +} + +func WithLabelFunc(labelFunc func(info rpcinfo.RPCInfo) []label.CwLabel) Option { + return option(func(cfg *Config) { + cfg.labelFunc = labelFunc + }) +} + +func WithEnableGRPCMetadata() Option { + return option(func(cfg *Config) { + cfg.enableGRPCMetadata = true + }) +} diff --git a/telemetry/instrumentation/otelkitex/peer.go b/telemetry/instrumentation/otelkitex/peer.go new file mode 100644 index 0000000..48e86b0 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/peer.go @@ -0,0 +1,83 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "context" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + "github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/metadata" + + "github.com/bytedance/gopkg/cloud/metainfo" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" +) + +func injectPeerServiceToMetaInfo(ctx context.Context, attrs []attribute.KeyValue) map[string]string { + md := metainfo.GetAllValues(ctx) + if md == nil { + md = make(map[string]string) + } + + serviceName, serviceNamespace, deploymentEnv := getServiceFromResourceAttributes(attrs) + + if serviceName != "" { + md[string(semconv.ServiceNameKey)] = serviceName + } + + if serviceNamespace != "" { + md[string(semconv.ServiceNamespaceKey)] = serviceNamespace + } + + if deploymentEnv != "" { + md[string(semconv.DeploymentEnvironmentKey)] = deploymentEnv + } + + return md +} + +func extractPeerServiceAttributesFromMetaInfo(md map[string]string) []attribute.KeyValue { + var attrs []attribute.KeyValue + + for k, v := range md { + switch k { + case string(semconv.ServiceNameKey): + attrs = append(attrs, semconv.PeerServiceKey.String(v)) + case string(semconv.ServiceNamespaceKey): + attrs = append(attrs, semantic.PeerServiceNamespaceKey.String(v)) + case string(semconv.DeploymentEnvironmentKey): + attrs = append(attrs, semantic.PeerDeploymentEnvironmentKey.String(v)) + } + } + + return attrs +} + +func extractPeerServiceAttributesFromMetadata(md metadata.MD) []attribute.KeyValue { + var ( + attrs []attribute.KeyValue + mdSupplier = metadataSupplier{metadata: &md} + ) + if v := mdSupplier.Get(string(semconv.ServiceNameKey)); v != "" { + attrs = append(attrs, semconv.PeerServiceKey.String(v)) + } + if v := mdSupplier.Get(string(semconv.ServiceNamespaceKey)); v != "" { + attrs = append(attrs, semantic.PeerServiceNamespaceKey.String(v)) + } + if v := mdSupplier.Get(string(semconv.DeploymentEnvironmentKey)); v != "" { + attrs = append(attrs, semantic.PeerDeploymentEnvironmentKey.String(v)) + } + return attrs +} diff --git a/telemetry/instrumentation/otelkitex/peer_test.go b/telemetry/instrumentation/otelkitex/peer_test.go new file mode 100644 index 0000000..18f5226 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/peer_test.go @@ -0,0 +1,118 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "context" + "testing" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" +) + +func Test_extractPeerServiceAttributesFromMetaInfo(t *testing.T) { + type args struct { + md map[string]string + } + tests := []struct { + name string + args args + want []attribute.KeyValue + wantCanonicalService string + }{ + { + name: "peer service", + args: args{ + md: map[string]string{ + string(semconv.ServiceNameKey): "foo", + }, + }, + want: []attribute.KeyValue{ + semconv.PeerServiceKey.String("foo"), + }, + }, + { + name: "full peer", + args: args{ + md: map[string]string{ + string(semconv.ServiceNameKey): "foo", + string(semconv.ServiceNamespaceKey): "test-ns", + string(semconv.DeploymentEnvironmentKey): "test-env", + }, + }, + want: []attribute.KeyValue{ + semconv.PeerServiceKey.String("foo"), + semantic.PeerServiceNamespaceKey.String("test-ns"), + semantic.PeerDeploymentEnvironmentKey.String("test-env"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractPeerServiceAttributesFromMetaInfo(tt.args.md) + assert.ElementsMatch(t, got, tt.want) + }) + } +} + +func Test_injectPeerServiceToMetaInfo(t *testing.T) { + type args struct { + ctx context.Context + attrs []attribute.KeyValue + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "peer service", + args: args{ + ctx: context.Background(), + attrs: []attribute.KeyValue{ + semconv.ServiceNameKey.String("foo"), + }, + }, + want: map[string]string{ + "service.name": "foo", + }, + }, + { + name: "full peer", + args: args{ + ctx: context.Background(), + attrs: []attribute.KeyValue{ + semconv.ServiceNameKey.String("foo"), + semconv.ServiceNamespaceKey.String("test-ns"), + semconv.DeploymentEnvironmentKey.String("test-env"), + }, + }, + want: map[string]string{ + string(semconv.ServiceNameKey): "foo", + string(semconv.ServiceNamespaceKey): "test-ns", + string(semconv.DeploymentEnvironmentKey): "test-env", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := injectPeerServiceToMetaInfo(tt.args.ctx, tt.args.attrs) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/telemetry/instrumentation/otelkitex/propagator.go b/telemetry/instrumentation/otelkitex/propagator.go new file mode 100644 index 0000000..6ab5e9f --- /dev/null +++ b/telemetry/instrumentation/otelkitex/propagator.go @@ -0,0 +1,84 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "context" + + "github.com/bytedance/gopkg/cloud/metainfo" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +var _ propagation.TextMapCarrier = &metadataProvider{} + +type metadataProvider struct { + metadata map[string]string +} + +// Get a value from metadata by key +func (m *metadataProvider) Get(key string) string { + if v, ok := m.metadata[key]; ok { + return v + } + return "" +} + +// Set a value to metadata by k/v +func (m *metadataProvider) Set(key, value string) { + m.metadata[key] = value +} + +// Keys Iteratively get all keys of metadata +func (m *metadataProvider) Keys() []string { + out := make([]string, 0, len(m.metadata)) + for k := range m.metadata { + out = append(out, k) + } + return out +} + +// Inject injects span context into the otelkitex metadata info +func Inject(ctx context.Context, c *Config, metadata map[string]string) { + c.GetTextMapPropagator().Inject(ctx, &metadataProvider{metadata: metadata}) +} + +// Extract returns the baggage and span context +func Extract(ctx context.Context, c *Config, metadata map[string]string) (baggage.Baggage, trace.SpanContext) { + ctx = c.GetTextMapPropagator().Extract(ctx, &metadataProvider{metadata: CGIVariableToHTTPHeaderMetadata(metadata)}) + return baggage.FromContext(ctx), trace.SpanContextFromContext(ctx) +} + +// CGIVariableToHTTPHeaderMetadata converts all CGI variable into HTTP header key. +// For example, `ABC_DEF` will be converted to `abc-def`. +func CGIVariableToHTTPHeaderMetadata(metadata map[string]string) map[string]string { + res := make(map[string]string, len(metadata)) + for k, v := range metadata { + res[metainfo.CGIVariableToHTTPHeader(k)] = v + } + return res +} + +// ExtractFromPropagator get metadata from propagator +func ExtractFromPropagator(ctx context.Context) map[string]string { + metadata := metainfo.GetAllValues(ctx) + if metadata == nil { + metadata = make(map[string]string) + } + otel.GetTextMapPropagator().Inject(ctx, &metadataProvider{metadata: metadata}) + return metadata +} diff --git a/telemetry/instrumentation/otelkitex/propagator_test.go b/telemetry/instrumentation/otelkitex/propagator_test.go new file mode 100644 index 0000000..6420480 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/propagator_test.go @@ -0,0 +1,201 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "context" + "reflect" + "testing" + + "github.com/bytedance/gopkg/cloud/metainfo" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/contrib/propagators/ot" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/baggage" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +func TestExtract(t *testing.T) { + ctx := context.Background() + bags, _ := baggage.Parse("foo=bar") + ctx = baggage.ContextWithBaggage(ctx, bags) + ctx = metainfo.WithValue(ctx, "foo", "bar") + + type args struct { + ctx context.Context + c *Config + metadata map[string]string + } + tests := []struct { + name string + args args + want baggage.Baggage + want1 trace.SpanContext + }{ + { + name: "extract successful", + args: args{ + ctx: ctx, + c: DefaultConfig(), + metadata: map[string]string{ + "foo": "bar", + }, + }, + want: bags, + want1: trace.SpanContext{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := Extract(tt.args.ctx, tt.args.c, tt.args.metadata) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Extract() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("Extract() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestInject(t *testing.T) { + cfg := NewConfig([]Option{WithTextMapPropagator(propagation.NewCompositeTextMapPropagator( + b3.New(), + ot.OT{}, + propagation.Baggage{}, + propagation.TraceContext{}, + ))}) + + ctx := context.Background() + + spanContext := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: [16]byte{1}, + SpanID: [8]byte{2}, + TraceFlags: 0, + TraceState: trace.TraceState{}, + Remote: false, + }) + + ctx = trace.ContextWithSpanContext(ctx, spanContext) + md := make(map[string]string) + + type args struct { + ctx context.Context + c *Config + metadata map[string]string + } + tests := []struct { + name string + args args + }{ + { + name: "inject valid", + args: args{ + ctx: ctx, + c: cfg, + metadata: md, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Inject(tt.args.ctx, tt.args.c, tt.args.metadata) + assert.NotEmpty(t, tt.args.metadata) + assert.Equal(t, "00-01000000000000000000000000000000-0200000000000000-00", tt.args.metadata["traceparent"]) + }) + } +} + +func TestCGIVariableToHTTPHeaderMetadata(t *testing.T) { + type args struct { + metadata map[string]string + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "HTTP2 CGI Variable", + args: args{ + metadata: map[string]string{ + "OT_BAGGAGE_SERVICE.NAME": "echo-client", + }, + }, + want: map[string]string{ + "ot-baggage-service.name": "echo-client", + }, + }, + { + name: "TTHeader", + args: args{ + metadata: map[string]string{ + "ot-baggage-service.name": "echo-client", + }, + }, + want: map[string]string{ + "ot-baggage-service.name": "echo-client", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CGIVariableToHTTPHeaderMetadata(tt.args.metadata); !reflect.DeepEqual(got, tt.want) { + t.Errorf("CGIVariableToHTTPHeaderMetadata() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestExtractFromPropagator(t *testing.T) { + otel.SetTextMapPropagator(propagation.Baggage{}) + + type args struct { + ctx context.Context + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "valid", + args: args{ + ctx: metainfo.WithValue(context.Background(), "baggage", "foo=bar"), + }, + want: map[string]string{ + "baggage": "foo=bar", + }, + }, + { + name: "not valid", + args: args{ + ctx: metainfo.WithValue(context.Background(), "other", "foo=bar"), + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := otel.GetTextMapPropagator().Extract( + tt.args.ctx, + &metadataProvider{metadata: ExtractFromPropagator(tt.args.ctx)}, + ) + assert.Equal(t, baggage.FromContext(ctx).String(), tt.want["baggage"]) + }) + } +} diff --git a/telemetry/instrumentation/otelkitex/suite.go b/telemetry/instrumentation/otelkitex/suite.go new file mode 100644 index 0000000..7d60388 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/suite.go @@ -0,0 +1,106 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "github.com/cloudwego/kitex/client" + "github.com/cloudwego/kitex/pkg/transmeta" + "github.com/cloudwego/kitex/server" + "github.com/cloudwego/kitex/transport" +) + +var ( + _ client.Suite = (*clientSuite)(nil) + _ server.Suite = (*serverSuite)(nil) +) + +type clientSuite struct { + cOpts []client.Option +} + +func (c *clientSuite) Options() []client.Option { + return c.cOpts +} + +type serverSuite struct { + sOpts []server.Option +} + +func (s *serverSuite) Options() []server.Option { + return s.sOpts +} + +// NewClientSuite client suite for otel with http2 and ttheader meta handler +func NewClientSuite(opts ...Option) *clientSuite { + clientOpts, cfg := NewClientOption(opts...) + cOpts := []client.Option{ + clientOpts, + client.WithMiddleware(ClientMiddleware(cfg)), + client.WithTransportProtocol(transport.TTHeader), + client.WithMetaHandler(transmeta.ClientHTTP2Handler), + client.WithMetaHandler(transmeta.ClientTTHeaderHandler), + } + return &clientSuite{cOpts} +} + +// NewServerSuite server suite for otel with http2 and ttheader meta handler +func NewServerSuite(opts ...Option) *serverSuite { + serverOpts, cfg := NewServerOption(opts...) + sOpts := []server.Option{ + serverOpts, + server.WithMiddleware(ServerMiddleware(cfg)), + server.WithMetaHandler(transmeta.ServerHTTP2Handler), + server.WithMetaHandler(transmeta.ServerTTHeaderHandler), + } + + return &serverSuite{sOpts} +} + +// Deprecated: Use NewServerSuite instead. +func NewGRPCServerSuite(opts ...Option) *serverSuite { + serverOpts, cfg := NewServerOption(opts...) + sOpts := []server.Option{ + serverOpts, + server.WithMiddleware(ServerMiddleware(cfg)), + server.WithMetaHandler(transmeta.ServerHTTP2Handler), + } + + return &serverSuite{sOpts} +} + +// Deprecated: Use NewClientSuite instead. +func NewGRPCClientSuite(opts ...Option) *clientSuite { + clientOpts, cfg := NewClientOption(opts...) + cOpts := []client.Option{ + clientOpts, + client.WithMiddleware(ClientMiddleware(cfg)), + client.WithTransportProtocol(transport.GRPC), + client.WithMetaHandler(transmeta.ClientHTTP2Handler), + client.WithMetaHandler(transmeta.ClientTTHeaderHandler), + } + return &clientSuite{cOpts} +} + +// Deprecated: Use NewClientSuite instead. +func NewFramedClientSuite(opts ...Option) *clientSuite { + clientOpts, cfg := NewClientOption(opts...) + cOpts := []client.Option{ + clientOpts, + client.WithMiddleware(ClientMiddleware(cfg)), + client.WithTransportProtocol(transport.Framed), + client.WithMetaHandler(transmeta.ClientTTHeaderHandler), + } + return &clientSuite{cOpts} +} diff --git a/telemetry/instrumentation/otelkitex/tracer.go b/telemetry/instrumentation/otelkitex/tracer.go new file mode 100644 index 0000000..81b109f --- /dev/null +++ b/telemetry/instrumentation/otelkitex/tracer.go @@ -0,0 +1,38 @@ +/* + * Copyright 2021 CloudWeGo 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. + */ + +// Package prometheus provides the extend implement of prometheus. +package otelkitex + +// NewServerTracer provides tracer for server access, addr and path is the scrape_configs for prometheus server. +func NewServerTracer(options ...Option) *KitexTracer { + cfg := NewConfig(options) + + return &KitexTracer{ + measure: cfg.measure, + cfg: cfg, + } +} + +// NewClientTracer provides tracer for server access, addr and path is the scrape_configs for prometheus server. +func NewClientTracer(options ...Option) *KitexTracer { + cfg := NewConfig(options) + + return &KitexTracer{ + measure: cfg.measure, + cfg: cfg, + } +} diff --git a/telemetry/instrumentation/otelkitex/tracer_client.go b/telemetry/instrumentation/otelkitex/tracer_client.go new file mode 100644 index 0000000..5514a88 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/tracer_client.go @@ -0,0 +1,25 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "github.com/cloudwego/kitex/client" +) + +func NewClientOption(opts ...Option) (client.Option, *Config) { + kitexTracer := NewClientTracer(opts...) + + return client.WithTracer(kitexTracer), kitexTracer.cfg +} diff --git a/telemetry/instrumentation/otelkitex/tracer_server.go b/telemetry/instrumentation/otelkitex/tracer_server.go new file mode 100644 index 0000000..d9e0689 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/tracer_server.go @@ -0,0 +1,24 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "github.com/cloudwego/kitex/server" +) + +func NewServerOption(opts ...Option) (server.Option, *Config) { + kitexTracer := NewServerTracer(opts...) + return server.WithTracer(kitexTracer), kitexTracer.cfg +} diff --git a/telemetry/instrumentation/otelkitex/utils.go b/telemetry/instrumentation/otelkitex/utils.go new file mode 100644 index 0000000..3b82bc9 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/utils.go @@ -0,0 +1,102 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "errors" + "fmt" + "time" + + "github.com/cloudwego/kitex/pkg/rpcinfo" + "github.com/cloudwego/kitex/pkg/stats" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" + "go.opentelemetry.io/otel/trace" +) + +// Ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#span-name +// naming rule: $package.$service/$method +func spanNaming(ri rpcinfo.RPCInfo) string { + if ri.Invocation().PackageName() != "" { + return ri.Invocation().PackageName() + "." + ri.Invocation().ServiceName() + "/" + ri.Invocation().MethodName() + } + return ri.Invocation().ServiceName() + "/" + ri.Invocation().MethodName() +} + +// recordErrorSpanWithStack record error with stack +func recordErrorSpanWithStack(span trace.Span, err error, stackMessage, stackTrace string, attributes ...attribute.KeyValue) { + if span == nil { + return + } + + // compatible with the case where error is empty + if err == nil { + err = errors.New(stackMessage) + } + + // stack trace + attributes = append(attributes, + semconv.ExceptionStacktraceKey.String(stackTrace), + ) + + span.SetStatus(codes.Error, err.Error()) + span.RecordError( + err, + trace.WithAttributes(attributes...), + ) +} + +func parseRPCError(ri rpcinfo.RPCInfo) (panicMsg, panicStack string, err error) { + panicked, panicErr := ri.Stats().Panicked() + if err = ri.Stats().Error(); err == nil && !panicked { + return + } + if panicked { + panicMsg = fmt.Sprintf("%v", panicErr) + if stackErr, ok := panicErr.(interface{ Stack() string }); ok { + panicStack = stackErr.Stack() + } + } + return +} + +func getStartTimeOrNow(ri rpcinfo.RPCInfo) time.Time { + if event := ri.Stats().GetEvent(stats.RPCStart); event != nil { + return event.Time() + } + return time.Now() +} + +func getEndTimeOrNow(ri rpcinfo.RPCInfo) time.Time { + if event := ri.Stats().GetEvent(stats.RPCFinish); event != nil { + return event.Time() + } + return time.Now() +} + +func getServiceFromResourceAttributes(attrs []attribute.KeyValue) (serviceName, serviceNamespace, deploymentEnv string) { + for _, attr := range attrs { + switch attr.Key { + case semconv.ServiceNameKey: + serviceName = attr.Value.AsString() + case semconv.ServiceNamespaceKey: + serviceNamespace = attr.Value.AsString() + case semconv.DeploymentEnvironmentKey: + deploymentEnv = attr.Value.AsString() + } + } + return +} diff --git a/telemetry/instrumentation/otelkitex/utils_test.go b/telemetry/instrumentation/otelkitex/utils_test.go new file mode 100644 index 0000000..aa766f4 --- /dev/null +++ b/telemetry/instrumentation/otelkitex/utils_test.go @@ -0,0 +1,127 @@ +// Copyright 2022 CloudWeGo 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. + +package otelkitex + +import ( + "context" + "errors" + "testing" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" +) + +func Test_getServiceFromResourceAttributes(t *testing.T) { + type args struct { + attrs []attribute.KeyValue + } + tests := []struct { + name string + args args + wantServiceName string + wantServiceNamespace string + wantDeploymentEnv string + }{ + { + name: "valid", + args: args{ + attrs: []attribute.KeyValue{ + semconv.ServiceNameKey.String("foo"), + }, + }, + wantServiceName: "foo", + wantServiceNamespace: "", + wantDeploymentEnv: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotServiceName, gotServiceNamespace, gotDeploymentEnv := getServiceFromResourceAttributes(tt.args.attrs) + if gotServiceName != tt.wantServiceName { + t.Errorf("getServiceFromResourceAttributes() gotServiceName = %v, want %v", gotServiceName, tt.wantServiceName) + } + if gotServiceNamespace != tt.wantServiceNamespace { + t.Errorf("getServiceFromResourceAttributes() gotServiceNamespace = %v, want %v", gotServiceNamespace, tt.wantServiceNamespace) + } + if gotDeploymentEnv != tt.wantDeploymentEnv { + t.Errorf("getServiceFromResourceAttributes() gotDeploymentEnv = %v, want %v", gotDeploymentEnv, tt.wantDeploymentEnv) + } + }) + } +} + +func Test_recordErrorSpan(t *testing.T) { + sr := tracetest.NewSpanRecorder() + tp := trace.NewTracerProvider(trace.WithSpanProcessor(sr)) + defer tp.Shutdown(context.Background()) //nolint:errcheck + + type args struct { + err error + withStackTrace bool + attributes []attribute.KeyValue + } + tests := []struct { + name string + args args + wantEventsLen int + wantEventAttributesLen int + }{ + { + name: "empty attributes", + args: args{ + err: errors.New("mock error"), + withStackTrace: true, + attributes: nil, + }, + wantEventsLen: 1, + wantEventAttributesLen: 3, + }, + { + name: "with attributes", + args: args{ + err: errors.New("mock error"), + withStackTrace: true, + attributes: []attribute.KeyValue{ + semantic.RPCSystemKitex, + }, + }, + wantEventsLen: 1, + wantEventAttributesLen: 3, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, testSpan := tp.Tracer("test").Start(context.Background(), "test-span") + defer testSpan.End() + + recordErrorSpanWithStack(testSpan, tt.args.err, "mock panic", "mock stack") + + readOnlySpan := testSpan.(trace.ReadOnlySpan) + + assert.Equal(t, trace.Status{Code: codes.Error, Description: "mock error"}, readOnlySpan.Status()) + assert.Equal(t, tt.wantEventsLen, len(readOnlySpan.Events())) + for _, event := range readOnlySpan.Events() { + assert.Equal(t, "exception", event.Name) + assert.Equal(t, tt.wantEventAttributesLen, len(event.Attributes)) + } + }) + } +} diff --git a/telemetry/instrumentation/otellogrus/go.mod b/telemetry/instrumentation/otellogrus/go.mod new file mode 100644 index 0000000..40ff019 --- /dev/null +++ b/telemetry/instrumentation/otellogrus/go.mod @@ -0,0 +1,12 @@ +module github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otellogrus + +go 1.21 + +require ( + github.com/sirupsen/logrus v1.9.2 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 +) + diff --git a/telemetry/instrumentation/otellogrus/hook.go b/telemetry/instrumentation/otellogrus/hook.go new file mode 100644 index 0000000..e36bcf8 --- /dev/null +++ b/telemetry/instrumentation/otellogrus/hook.go @@ -0,0 +1,96 @@ +// Copyright 2022 CloudWeGo 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. + +package otellogrus + +import ( + "errors" + + "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// Ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/overview.md#json-formats +const ( + traceIDKey = "trace_id" + spanIDKey = "span_id" + traceFlagsKey = "trace_flags" +) + +var _ logrus.Hook = (*TraceHook)(nil) + +// TraceHookConfig trace hook config +type TraceHookConfig struct { + recordStackTraceInSpan bool + enableLevels []logrus.Level + errorSpanLevel logrus.Level +} + +// NewTraceHookConfig Constructor, used to create TraceHookConfig +func NewTraceHookConfig(recordStackTraceInSpan bool, enableLevels []logrus.Level, errorSpanLevel logrus.Level) *TraceHookConfig { + return &TraceHookConfig{ + recordStackTraceInSpan: recordStackTraceInSpan, + enableLevels: enableLevels, + errorSpanLevel: errorSpanLevel, + } +} + +// TraceHook trace hook +type TraceHook struct { + cfg *TraceHookConfig +} + +// NewTraceHook create trace hook +func NewTraceHook(cfg *TraceHookConfig) *TraceHook { + return &TraceHook{cfg: cfg} +} + +// Levels get levels +func (h *TraceHook) Levels() []logrus.Level { + return h.cfg.enableLevels +} + +// Fire otellogrus hook fire +func (h *TraceHook) Fire(entry *logrus.Entry) error { + if entry.Context == nil { + return nil + } + + span := trace.SpanFromContext(entry.Context) + + // check span context + spanContext := span.SpanContext() + if !spanContext.IsValid() { + return nil + } + + // attach span context to log entry data fields + entry.Data[traceIDKey] = spanContext.TraceID() + entry.Data[spanIDKey] = spanContext.SpanID() + entry.Data[traceFlagsKey] = spanContext.TraceFlags() + + // non recording spans do not support modifying + if !span.IsRecording() { + return nil + } + + // set span status + if entry.Level <= h.cfg.errorSpanLevel { + span.SetStatus(codes.Error, "") + span.RecordError(errors.New(entry.Message), trace.WithStackTrace(h.cfg.recordStackTraceInSpan)) + } + + return nil +} diff --git a/telemetry/instrumentation/otellogrus/logger.go b/telemetry/instrumentation/otellogrus/logger.go new file mode 100644 index 0000000..11bb318 --- /dev/null +++ b/telemetry/instrumentation/otellogrus/logger.go @@ -0,0 +1,44 @@ +// Copyright 2022 CloudWeGo 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. + +package otellogrus + +import ( + "github.com/cloudwego-contrib/cwgo-pkg/log/logging/logrus" +) + +// Logger an alias to github.com/hertz-contrib/logger/otellogrus Logger +type Logger = logrus.Logger + +// NewLogger create logger with otel hook +func NewLogger(opts ...Option) *Logger { + cfg := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(cfg) + } + + // default trace hooks + cfg.hooks = append(cfg.hooks, NewTraceHook(cfg.traceHookConfig)) + + // attach hook + for _, hook := range cfg.hooks { + cfg.logger.AddHook(hook) + } + + return logrus.NewLogger( + logrus.WithLogger(cfg.logger), + ) +} diff --git a/telemetry/instrumentation/otellogrus/logger_test.go b/telemetry/instrumentation/otellogrus/logger_test.go new file mode 100644 index 0000000..fb43217 --- /dev/null +++ b/telemetry/instrumentation/otellogrus/logger_test.go @@ -0,0 +1,103 @@ +// Copyright 2022 CloudWeGo 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. + +package otellogrus_test + +import ( + "context" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otellogrus" + + "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func stdoutProvider(ctx context.Context) func() { + provider := sdktrace.NewTracerProvider() + otel.SetTracerProvider(provider) + + exp, err := stdouttrace.New() + if err != nil { + panic(err) + } + + bsp := sdktrace.NewBatchSpanProcessor(exp) + provider.RegisterSpanProcessor(bsp) + + return func() { + if err := provider.Shutdown(ctx); err != nil { + panic(err) + } + } +} + +func TestLogger(t *testing.T) { + ctx := context.Background() + shutdown := stdoutProvider(ctx) + defer shutdown() + + logger := otellogrus.NewLogger( + otellogrus.WithTraceHookErrorSpanLevel(logrus.WarnLevel), + otellogrus.WithTraceHookLevels(logrus.AllLevels), + otellogrus.WithRecordStackTraceInSpan(true), + ) + + logger.Logger().Info("log from origin otellogrus") + + hlog.SetLogger(logger) + hlog.SetLevel(hlog.LevelDebug) + + tracer := otel.Tracer("test otel std logger") + ctx, span := tracer.Start(ctx, "root") + + hlog.SetLogger(logger) + hlog.SetLevel(hlog.LevelTrace) + + hlog.Trace("trace") + hlog.Debug("debug") + hlog.Info("info") + hlog.Notice("notice") + hlog.Warn("warn") + hlog.Error("error") + + hlog.Tracef("log level: %s", "trace") + hlog.Debugf("log level: %s", "debug") + hlog.Infof("log level: %s", "info") + hlog.Noticef("log level: %s", "notice") + hlog.Warnf("log level: %s", "warn") + hlog.Errorf("log level: %s", "error") + + hlog.CtxTracef(ctx, "log level: %s", "trace") + hlog.CtxDebugf(ctx, "log level: %s", "debug") + hlog.CtxInfof(ctx, "log level: %s", "info") + hlog.CtxNoticef(ctx, "log level: %s", "notice") + hlog.CtxWarnf(ctx, "log level: %s", "warn") + hlog.CtxErrorf(ctx, "log level: %s", "error") + + span.End() + + ctx, child := tracer.Start(ctx, "child") + hlog.CtxWarnf(ctx, "foo %s", "bar") + child.End() + + ctx, errSpan := tracer.Start(ctx, "error") + hlog.CtxErrorf(ctx, "error %s", "this is a error") + hlog.Info("no trace context") + errSpan.End() +} diff --git a/telemetry/instrumentation/otellogrus/option.go b/telemetry/instrumentation/otellogrus/option.go new file mode 100644 index 0000000..cefc2cf --- /dev/null +++ b/telemetry/instrumentation/otellogrus/option.go @@ -0,0 +1,96 @@ +// Copyright 2022 CloudWeGo 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. + +package otellogrus + +import ( + "github.com/sirupsen/logrus" +) + +// Option otellogrus hook option +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + logger *logrus.Logger + hooks []logrus.Hook + + traceHookConfig *TraceHookConfig +} + +func defaultConfig() *config { + // std logger + stdLogger := logrus.StandardLogger() + // default json format + stdLogger.SetFormatter(new(logrus.JSONFormatter)) + + return &config{ + logger: logrus.StandardLogger(), + hooks: []logrus.Hook{}, + traceHookConfig: &TraceHookConfig{ + recordStackTraceInSpan: true, + enableLevels: logrus.AllLevels, + errorSpanLevel: logrus.ErrorLevel, + }, + } +} + +// WithLogger configures logger +func WithLogger(logger *logrus.Logger) Option { + return option(func(cfg *config) { + cfg.logger = logger + }) +} + +// WithHook configures hook +func WithHook(hook logrus.Hook) Option { + return option(func(cfg *config) { + cfg.hooks = append(cfg.hooks, hook) + }) +} + +// WithTraceHookConfig configures trace hook config +func WithTraceHookConfig(hookConfig *TraceHookConfig) Option { + return option(func(cfg *config) { + cfg.traceHookConfig = hookConfig + }) +} + +// WithTraceHookLevels configures hook levels +func WithTraceHookLevels(levels []logrus.Level) Option { + return option(func(cfg *config) { + cfg.traceHookConfig.enableLevels = levels + }) +} + +// WithTraceHookErrorSpanLevel configures trace hook error span level +func WithTraceHookErrorSpanLevel(level logrus.Level) Option { + return option(func(cfg *config) { + cfg.traceHookConfig.errorSpanLevel = level + }) +} + +// WithRecordStackTraceInSpan configures whether record stack trace in span +func WithRecordStackTraceInSpan(recordStackTraceInSpan bool) Option { + return option(func(cfg *config) { + cfg.traceHookConfig.recordStackTraceInSpan = recordStackTraceInSpan + }) +} diff --git a/telemetry/instrumentation/otellogrus/util.go b/telemetry/instrumentation/otellogrus/util.go new file mode 100644 index 0000000..441767c --- /dev/null +++ b/telemetry/instrumentation/otellogrus/util.go @@ -0,0 +1,31 @@ +// Copyright 2022 CloudWeGo 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. + +package otellogrus + +import ( + "strings" + + "github.com/sirupsen/logrus" +) + +// OtelSeverityText convert otellogrus level to otel severityText +// ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#severity-fields +func OtelSeverityText(lv logrus.Level) string { + s := lv.String() + if s == "warning" { + s = "warn" + } + return strings.ToUpper(s) +} diff --git a/telemetry/instrumentation/otellogrus/util_test.go b/telemetry/instrumentation/otellogrus/util_test.go new file mode 100644 index 0000000..78fbc8b --- /dev/null +++ b/telemetry/instrumentation/otellogrus/util_test.go @@ -0,0 +1,47 @@ +// Copyright 2022 CloudWeGo 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. + +package otellogrus + +import ( + "testing" + + "github.com/sirupsen/logrus" +) + +func TestOtelSeverityText(t *testing.T) { + type args struct { + lv logrus.Level + } + tests := []struct { + name string + args args + want string + }{ + { + name: "warn", + args: args{ + lv: logrus.WarnLevel, + }, + want: "WARN", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := OtelSeverityText(tt.args.lv); got != tt.want { + t.Errorf("OtelSeverityText() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/telemetry/instrumentation/otelslog/go.mod b/telemetry/instrumentation/otelslog/go.mod new file mode 100644 index 0000000..5c202a0 --- /dev/null +++ b/telemetry/instrumentation/otelslog/go.mod @@ -0,0 +1,12 @@ +module github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelslog + +go 1.21 + +require ( + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 +) + diff --git a/telemetry/instrumentation/otelslog/handler.go b/telemetry/instrumentation/otelslog/handler.go new file mode 100644 index 0000000..5f4b4de --- /dev/null +++ b/telemetry/instrumentation/otelslog/handler.go @@ -0,0 +1,90 @@ +// Copyright 2022 CloudWeGo 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. + +package otelslog + +import ( + "context" + "errors" + "io" + "log/slog" + + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +const ( + traceIDKey = "trace_id" + spanIDKey = "span_id" + traceFlagsKey = "trace_flags" +) + +type traceConfig struct { + recordStackTraceInSpan bool + errorSpanLevel slog.Level +} + +type traceHandler struct { + slog.Handler + tcfg *traceConfig +} + +func NewTraceHandler(w io.Writer, opts *slog.HandlerOptions, traceConfig *traceConfig) *traceHandler { + if opts == nil { + opts = &slog.HandlerOptions{} + } + return &traceHandler{ + slog.NewJSONHandler(w, opts), + traceConfig, + } +} + +func (t *traceHandler) Enabled(ctx context.Context, level slog.Level) bool { + return t.Handler.Enabled(ctx, level) +} + +func (t *traceHandler) Handle(ctx context.Context, record slog.Record) error { + // trace span add + span := trace.SpanFromContext(ctx) + if span.SpanContext().TraceID().IsValid() { + record.Add(traceIDKey, span.SpanContext().TraceID()) + } + if span.SpanContext().SpanID().IsValid() { + record.Add(spanIDKey, span.SpanContext().SpanID()) + } + if span.SpanContext().TraceFlags().IsSampled() { + record.Add(traceFlagsKey, span.SpanContext().TraceFlags()) + } + + // non recording spans do not support modifying + if !span.IsRecording() { + return t.Handler.Handle(ctx, record) + } + + // set span status + if record.Level >= t.tcfg.errorSpanLevel { + span.SetStatus(codes.Error, "") + span.RecordError(errors.New(record.Message), trace.WithStackTrace(t.tcfg.recordStackTraceInSpan)) + } + + return t.Handler.Handle(ctx, record) +} + +func (t *traceHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return t.Handler.WithAttrs(attrs) +} + +func (t *traceHandler) WithGroup(name string) slog.Handler { + return t.Handler.WithGroup(name) +} diff --git a/telemetry/instrumentation/otelslog/logger.go b/telemetry/instrumentation/otelslog/logger.go new file mode 100644 index 0000000..990b715 --- /dev/null +++ b/telemetry/instrumentation/otelslog/logger.go @@ -0,0 +1,64 @@ +// Copyright 2023 CloudWeGo 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. + +package otelslog + +import ( + "io" + "log/slog" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + cwslog "github.com/cloudwego-contrib/cwgo-pkg/log/logging/slog" +) + +const ( + LevelTrace = slog.Level(-8) + LevelNotice = slog.Level(2) + LevelFatal = slog.Level(12) +) + +type Logger struct { + cwslog.Logger + config *config +} + +func NewLogger(opts ...Option) *Logger { + cfg := defaultConfig() + + for _, opt := range opts { + opt.apply(cfg) + } + logger := &Logger{ + Logger: *cfg.logger, + config: cfg, + } + logger.setTraceLogger() + return logger +} + +func (l *Logger) setTraceLogger() { + log := slog.New(NewTraceHandler(l.GetOutput(), l.config.logger.GetHandler(), l.config.traceConfig)) + l.Logger.SetLogger(log) +} + +func (l *Logger) SetOutput(writer io.Writer) { + log := slog.New(NewTraceHandler(writer, l.config.logger.GetHandler(), l.config.traceConfig)) + l.config.logger.SetOutput(writer) + l.Logger.SetLogger(log) +} + +func (l *Logger) SetLevel(level hlog.Level) { + l.Logger.SetLevel(level) +} diff --git a/telemetry/instrumentation/otelslog/logger_test.go b/telemetry/instrumentation/otelslog/logger_test.go new file mode 100644 index 0000000..973d025 --- /dev/null +++ b/telemetry/instrumentation/otelslog/logger_test.go @@ -0,0 +1,155 @@ +// Copyright 2023 CloudWeGo 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. + +package otelslog + +import ( + "bytes" + "context" + "log/slog" + "strings" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + cwslog "github.com/cloudwego-contrib/cwgo-pkg/log/logging/slog" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func stdoutProvider(ctx context.Context) func() { + provider := sdktrace.NewTracerProvider() + otel.SetTracerProvider(provider) + + exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + panic(err) + } + + bsp := sdktrace.NewBatchSpanProcessor(exp) + provider.RegisterSpanProcessor(bsp) + + return func() { + if err := provider.Shutdown(ctx); err != nil { + panic(err) + } + } +} + +func TestLogger(t *testing.T) { + ctx := context.Background() + + buf := new(bytes.Buffer) + + shutdown := stdoutProvider(ctx) + defer shutdown() + + logger := NewLogger( + WithTraceErrorSpanLevel(slog.LevelWarn), + WithRecordStackTraceInSpan(true), + ) + + hlog.SetLogger(logger) + hlog.SetOutput(buf) + hlog.SetLevel(hlog.LevelDebug) + + logger.Info("log from origin otelslog") + assert.True(t, strings.Contains(buf.String(), "log from origin otelslog")) + buf.Reset() + + tracer := otel.Tracer("test otel std logger") + + ctx, span := tracer.Start(ctx, "root") + + hlog.CtxInfof(ctx, "hello %s", "you") + assert.True(t, strings.Contains(buf.String(), "trace_id")) + assert.True(t, strings.Contains(buf.String(), "span_id")) + assert.True(t, strings.Contains(buf.String(), "trace_flags")) + + buf.Reset() + + span.End() + + ctx, child := tracer.Start(ctx, "child") + + hlog.CtxWarnf(ctx, "foo %s", "bar") + + hlog.CtxTracef(ctx, "trace %s", "this is a trace log") + hlog.CtxDebugf(ctx, "debug %s", "this is a debug log") + hlog.CtxInfof(ctx, "info %s", "this is a info log") + hlog.CtxNoticef(ctx, "notice %s", "this is a notice log") + hlog.CtxWarnf(ctx, "warn %s", "this is a warn log") + hlog.CtxErrorf(ctx, "error %s", "this is a error log") + + child.End() + + _, errSpan := tracer.Start(ctx, "error") + + hlog.Info("no trace context") + + errSpan.End() +} + +func TestLogLevel(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger( + WithTraceErrorSpanLevel(slog.LevelWarn), + WithRecordStackTraceInSpan(true), + ) + + // output to buffer + logger.SetOutput(buf) + + logger.Debug("this is a debug log") + assert.False(t, strings.Contains(buf.String(), "this is a debug log")) + + logger.SetLevel(hlog.LevelDebug) + + logger.Debugf("this is a debug log %s", "msg") + + assert.True(t, strings.Contains(buf.String(), "this is a debug log")) +} + +func TestLogOption(t *testing.T) { + buf := new(bytes.Buffer) + + lvl := new(slog.LevelVar) + lvl.Set(slog.LevelDebug) + logger := NewLogger( + WithLogger( + cwslog.NewLogger( + cwslog.WithLevel(lvl), + cwslog.WithOutput(buf), + cwslog.WithHandlerOptions(&slog.HandlerOptions{ + AddSource: false, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.MessageKey { + msg := a.Value.Any().(string) + msg = strings.ReplaceAll(msg, "log", "new log") + a.Value = slog.StringValue(msg) + } + return a + }, + }), + )), + WithTraceErrorSpanLevel(slog.LevelWarn), + WithRecordStackTraceInSpan(true), + ) + + logger.Debug("this is a debug log") + assert.True(t, strings.Contains(buf.String(), "this is a debug new log")) +} diff --git a/telemetry/instrumentation/otelslog/option.go b/telemetry/instrumentation/otelslog/option.go new file mode 100644 index 0000000..23489a9 --- /dev/null +++ b/telemetry/instrumentation/otelslog/option.go @@ -0,0 +1,68 @@ +// Copyright 2023 CloudWeGo 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. + +package otelslog + +import ( + "log/slog" + + cwslog "github.com/cloudwego-contrib/cwgo-pkg/log/logging/slog" +) + +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + logger *cwslog.Logger + traceConfig *traceConfig +} + +// defaultConfig default config +func defaultConfig() *config { + return &config{ + traceConfig: &traceConfig{ + recordStackTraceInSpan: true, + errorSpanLevel: slog.LevelError, + }, + logger: cwslog.NewLogger(), + } +} + +// WithLogger configures logger +func WithLogger(logger *cwslog.Logger) Option { + return option(func(cfg *config) { + cfg.logger = logger + }) +} + +// WithTraceErrorSpanLevel trace error span level option +func WithTraceErrorSpanLevel(level slog.Level) Option { + return option(func(cfg *config) { + cfg.traceConfig.errorSpanLevel = level + }) +} + +// WithRecordStackTraceInSpan record stack track option +func WithRecordStackTraceInSpan(recordStackTraceInSpan bool) Option { + return option(func(cfg *config) { + cfg.traceConfig.recordStackTraceInSpan = recordStackTraceInSpan + }) +} diff --git a/telemetry/instrumentation/otelslog/utils.go b/telemetry/instrumentation/otelslog/utils.go new file mode 100644 index 0000000..d0e010a --- /dev/null +++ b/telemetry/instrumentation/otelslog/utils.go @@ -0,0 +1,55 @@ +// Copyright 2023 CloudWeGo 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. + +package otelslog + +import ( + "log/slog" + "strings" + + "github.com/cloudwego/hertz/pkg/common/hlog" +) + +// OtelSeverityText convert otelslog level to otel severityText +// ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#severity-fields +func OtelSeverityText(lv slog.Level) string { + s := lv.String() + if s == "warning" { + s = "warn" + } + return strings.ToUpper(s) +} + +// TranSLevel Adapt klog level to teleology level +func TranSLevel(level hlog.Level) (lvl slog.Level) { + switch level { + case hlog.LevelTrace: + lvl = LevelTrace + case hlog.LevelDebug: + lvl = slog.LevelDebug + case hlog.LevelInfo: + lvl = slog.LevelInfo + case hlog.LevelWarn: + lvl = slog.LevelWarn + case hlog.LevelNotice: + lvl = LevelNotice + case hlog.LevelError: + lvl = slog.LevelError + case hlog.LevelFatal: + lvl = LevelFatal + default: + lvl = slog.LevelWarn + } + return +} diff --git a/telemetry/instrumentation/otelzap/go.mod b/telemetry/instrumentation/otelzap/go.mod new file mode 100644 index 0000000..74f3899 --- /dev/null +++ b/telemetry/instrumentation/otelzap/go.mod @@ -0,0 +1,12 @@ +module github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelzap + +go 1.21 + +require ( + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 + go.uber.org/zap v1.24.0 +) diff --git a/telemetry/instrumentation/otelzap/logger.go b/telemetry/instrumentation/otelzap/logger.go new file mode 100644 index 0000000..9b3eb07 --- /dev/null +++ b/telemetry/instrumentation/otelzap/logger.go @@ -0,0 +1,144 @@ +// Copyright 2022 CloudWeGo 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. + +package otelzap + +import ( + "context" + "errors" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + cwzap "github.com/cloudwego-contrib/cwgo-pkg/log/logging/zap" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Logger struct { + cwzap.Logger + config *config +} + +// Ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md#json-formats +const ( + traceIDKey = "trace_id" + spanIDKey = "span_id" + traceFlagsKey = "trace_flags" +) + +var extraKeys = []cwzap.ExtraKey{traceIDKey, spanIDKey, traceFlagsKey} + +func NewLogger(opts ...Option) *Logger { + config := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(config) + } + logger := *config.logger + if config.hasCwZap { + options := GetOptions(config.cwZap) + logger = *cwzap.NewLogger(options...) + extraKeys = append(extraKeys, config.cwZap.extraKeys...) + } + logger.PutExtraKeys(extraKeys...) + + return &Logger{ + Logger: logger, + config: config, + } +} + +func (l *Logger) CtxLogf(level hlog.Level, ctx context.Context, format string, kvs ...interface{}) { + var zlevel zapcore.Level + span := trace.SpanFromContext(ctx) + + if span.SpanContext().IsValid() { + ctx = context.WithValue(ctx, cwzap.ExtraKey(traceIDKey), span.SpanContext().TraceID()) + ctx = context.WithValue(ctx, cwzap.ExtraKey(spanIDKey), span.SpanContext().SpanID()) + ctx = context.WithValue(ctx, cwzap.ExtraKey(traceFlagsKey), span.SpanContext().TraceFlags()) + + l.Logger.CtxLogf(level, ctx, format, kvs...) + } else { + l.Logger.Logf(level, format, kvs...) + } + + if !span.IsRecording() { + return + } + + switch level { + case hlog.LevelDebug, hlog.LevelTrace: + zlevel = zap.DebugLevel + case hlog.LevelInfo: + zlevel = zap.InfoLevel + case hlog.LevelNotice, hlog.LevelWarn: + zlevel = zap.WarnLevel + case hlog.LevelError: + zlevel = zap.ErrorLevel + case hlog.LevelFatal: + zlevel = zap.FatalLevel + default: + zlevel = zap.WarnLevel + } + + // set span status + if zlevel >= l.config.traceConfig.errorSpanLevel { + msg := getMessage(format, kvs) + span.SetStatus(codes.Error, "") + span.RecordError(errors.New(msg), trace.WithStackTrace(l.config.traceConfig.recordStackTraceInSpan)) + } +} + +func (l *Logger) CtxTracef(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxDebugf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelDebug, ctx, format, v...) +} + +func (l *Logger) CtxInfof(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelInfo, ctx, format, v...) +} + +func (l *Logger) CtxNoticef(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelWarn, ctx, format, v...) +} + +func (l *Logger) CtxWarnf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelWarn, ctx, format, v...) +} + +func (l *Logger) CtxErrorf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelError, ctx, format, v...) +} + +func (l *Logger) CtxFatalf(ctx context.Context, format string, v ...interface{}) { + l.CtxLogf(hlog.LevelFatal, ctx, format, v...) +} + +func GetOptions(cwZap cwZap) []cwzap.Option { + opions := []cwzap.Option{} + opions = append(opions, cwzap.WithCores(cwzap.CoreConfig{ + Enc: cwZap.coreConfig.Enc, + Lvl: cwZap.coreConfig.Lvl, + Ws: cwZap.coreConfig.Ws, + })) + opions = append(opions, cwzap.WithZapOptions(cwZap.zapOpts...)) + opions = append(opions, cwzap.WithCustomFields(cwZap.customFields)) + return opions +} diff --git a/telemetry/instrumentation/otelzap/logger_test.go b/telemetry/instrumentation/otelzap/logger_test.go new file mode 100644 index 0000000..e697e22 --- /dev/null +++ b/telemetry/instrumentation/otelzap/logger_test.go @@ -0,0 +1,130 @@ +// Copyright 2022 CloudWeGo 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. + +package otelzap + +import ( + "bytes" + "context" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/zap" +) + +func stdoutProvider(ctx context.Context) func() { + provider := sdktrace.NewTracerProvider() + otel.SetTracerProvider(provider) + + exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + panic(err) + } + + bsp := sdktrace.NewBatchSpanProcessor(exp) + provider.RegisterSpanProcessor(bsp) + + return func() { + if err := provider.Shutdown(ctx); err != nil { + panic(err) + } + } +} + +// TestLogger test logger work with opentelemetry +func TestLogger(t *testing.T) { + ctx := context.Background() + + buf := new(bytes.Buffer) + + shutdown := stdoutProvider(ctx) + defer shutdown() + + logger := NewLogger( + WithTraceErrorSpanLevel(zap.WarnLevel), + WithRecordStackTraceInSpan(true), + ) + defer logger.Sync() + + hlog.SetLogger(logger) + hlog.SetOutput(buf) + hlog.SetLevel(hlog.LevelDebug) + + logger.Info("log from origin otelzap") + assert.Contains(t, buf.String(), "log from origin otelzap") + buf.Reset() + + tracer := otel.Tracer("test otel std logger") + + ctx, span := tracer.Start(ctx, "root") + + hlog.CtxInfof(ctx, "hello %s", "world") + assert.Contains(t, buf.String(), "trace_id") + assert.Contains(t, buf.String(), "span_id") + assert.Contains(t, buf.String(), "trace_flags") + buf.Reset() + + span.End() + + ctx, child1 := tracer.Start(ctx, "child1") + + hlog.CtxTracef(ctx, "trace %s", "this is a trace log") + hlog.CtxDebugf(ctx, "debug %s", "this is a debug log") + hlog.CtxInfof(ctx, "info %s", "this is a info log") + + child1.End() + assert.Equal(t, codes.Unset, child1.(sdktrace.ReadOnlySpan).Status().Code) + + ctx, child2 := tracer.Start(ctx, "child2") + hlog.CtxNoticef(ctx, "notice %s", "this is a notice log") + hlog.CtxWarnf(ctx, "warn %s", "this is a warn log") + hlog.CtxErrorf(ctx, "error %s", "this is a error log") + + child2.End() + assert.Equal(t, codes.Error, child2.(sdktrace.ReadOnlySpan).Status().Code) + + _, errSpan := tracer.Start(ctx, "error") + + hlog.Info("no trace context") + + errSpan.End() +} + +// TestLogLevel test SetLevel +func TestLogLevel(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger( + WithTraceErrorSpanLevel(zap.WarnLevel), + WithRecordStackTraceInSpan(true), + ) + defer logger.Sync() + + // output to buffer + logger.SetOutput(buf) + + logger.Debug("this is a debug log") + assert.NotContains(t, buf.String(), "this is a debug log") + + logger.SetLevel(hlog.LevelDebug) + + logger.Debugf("this is a debug log %s", "msg") + assert.Contains(t, buf.String(), "this is a debug log") +} diff --git a/telemetry/instrumentation/otelzap/option.go b/telemetry/instrumentation/otelzap/option.go new file mode 100644 index 0000000..b262449 --- /dev/null +++ b/telemetry/instrumentation/otelzap/option.go @@ -0,0 +1,124 @@ +// Copyright 2022 CloudWeGo 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. + +package otelzap + +import ( + cwzap "github.com/cloudwego-contrib/cwgo-pkg/log/logging/zap" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type traceConfig struct { + recordStackTraceInSpan bool + errorSpanLevel zapcore.Level +} + +// cwZap is for compatibility with Kitex otel log +type cwZap struct { + customFields []interface{} + extraKeys []cwzap.ExtraKey + coreConfig cwzap.CoreConfig + zapOpts []zap.Option +} + +type config struct { + logger *cwzap.Logger + traceConfig *traceConfig + cwZap cwZap + hasCwZap bool +} + +// defaultConfig default config +func defaultConfig() *config { + return &config{ + traceConfig: &traceConfig{ + recordStackTraceInSpan: true, + errorSpanLevel: zapcore.ErrorLevel, + }, + logger: cwzap.NewLogger(), + } +} + +// WithCoreEnc zapcore encoder +func WithCoreEnc(enc zapcore.Encoder) Option { + return option(func(cfg *config) { + cfg.cwZap.coreConfig.Enc = enc + cfg.hasCwZap = true + }) +} + +// WithCoreWs zapcore write syncer +func WithCoreWs(ws zapcore.WriteSyncer) Option { + return option(func(cfg *config) { + cfg.cwZap.coreConfig.Ws = ws + cfg.hasCwZap = true + }) +} + +// WithCoreLevel zapcore log level +func WithCoreLevel(lvl zap.AtomicLevel) Option { + return option(func(cfg *config) { + cfg.cwZap.coreConfig.Lvl = lvl + cfg.hasCwZap = true + }) +} + +// WithCustomFields record log with the key-value pair. +func WithCustomFields(kv ...interface{}) Option { + return option(func(cfg *config) { + cfg.cwZap.customFields = append(cfg.cwZap.customFields, kv...) + cfg.hasCwZap = true + }) +} + +// WithZapOptions add origin zap option +func WithZapOptions(opts ...zap.Option) Option { + return option(func(cfg *config) { + cfg.cwZap.zapOpts = append(cfg.cwZap.zapOpts, opts...) + cfg.hasCwZap = true + }) +} + +// WithLogger configures logger +func WithLogger(logger *cwzap.Logger) Option { + return option(func(cfg *config) { + logger.PutExtraKeys(extraKeys...) + cfg.logger = logger + }) +} + +// WithTraceErrorSpanLevel trace error span level option +func WithTraceErrorSpanLevel(level zapcore.Level) Option { + return option(func(cfg *config) { + cfg.traceConfig.errorSpanLevel = level + }) +} + +// WithRecordStackTraceInSpan record stack track option +func WithRecordStackTraceInSpan(recordStackTraceInSpan bool) Option { + return option(func(cfg *config) { + cfg.traceConfig.recordStackTraceInSpan = recordStackTraceInSpan + }) +} diff --git a/telemetry/instrumentation/otelzap/option_test.go b/telemetry/instrumentation/otelzap/option_test.go new file mode 100644 index 0000000..0f281bb --- /dev/null +++ b/telemetry/instrumentation/otelzap/option_test.go @@ -0,0 +1,29 @@ +// Copyright 2022 CloudWeGo 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. + +package otelzap + +import ( + "testing" + + cwzap "github.com/cloudwego-contrib/cwgo-pkg/log/logging/zap" + "github.com/stretchr/testify/assert" +) + +func TestWithLogger(t *testing.T) { + l := NewLogger(WithLogger(cwzap.NewLogger())) + for _, v := range extraKeys { + assert.Contains(t, l.GetExtraKeys(), v) + } +} diff --git a/telemetry/instrumentation/otelzap/utils.go b/telemetry/instrumentation/otelzap/utils.go new file mode 100644 index 0000000..378e8cd --- /dev/null +++ b/telemetry/instrumentation/otelzap/utils.go @@ -0,0 +1,49 @@ +// Copyright 2022 CloudWeGo 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. + +package otelzap + +import ( + "fmt" + + "go.uber.org/zap/zapcore" +) + +// getMessage format with Sprint, Sprintf, or neither. +func getMessage(template string, fmtArgs []interface{}) string { + if len(fmtArgs) == 0 { + return template + } + + if template != "" { + return fmt.Sprintf(template, fmtArgs...) + } + + if len(fmtArgs) == 1 { + if str, ok := fmtArgs[0].(string); ok { + return str + } + } + return fmt.Sprint(fmtArgs...) +} + +// OtelSeverityText convert zapcore level to otel severityText +// ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#severity-fields +func OtelSeverityText(lv zapcore.Level) string { + s := lv.CapitalString() + if s == "DPANIC" || s == "PANIC" { + s = "FATAL" + } + return s +} diff --git a/telemetry/instrumentation/otelzerolog/go.mod b/telemetry/instrumentation/otelzerolog/go.mod new file mode 100644 index 0000000..31526d1 --- /dev/null +++ b/telemetry/instrumentation/otelzerolog/go.mod @@ -0,0 +1,13 @@ +module github.com/cloudwego-contrib/cwgo-pkg/telemetry/instrumentation/otelzerolog + +go 1.21 + +require ( + github.com/rs/zerolog v1.30.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 +) + diff --git a/telemetry/instrumentation/otelzerolog/logger.go b/telemetry/instrumentation/otelzerolog/logger.go new file mode 100644 index 0000000..5f30639 --- /dev/null +++ b/telemetry/instrumentation/otelzerolog/logger.go @@ -0,0 +1,52 @@ +// Copyright 2024 CloudWeGo 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. + +package otelzerolog + +import ( + cwzerolog "github.com/cloudwego-contrib/cwgo-pkg/log/logging/zerolog" +) + +type Logger struct { + *cwzerolog.Logger + config *config +} + +// Ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md#json-formats +const ( + traceIDKey = "trace_id" + spanIDKey = "span_id" + traceFlagsKey = "trace_flags" +) + +func NewLogger(opts ...Option) *Logger { + cfg := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(cfg) + } + logger := *cfg.logger + if cfg.zeroLogger != nil { + logger = *cwzerolog.From(*cfg.zeroLogger) + } + + zerologLogger := logger.Unwrap(). + Hook(cfg.defaultZerologHookFn()) + + return &Logger{ + Logger: cwzerolog.From(zerologLogger), + config: cfg, + } +} diff --git a/telemetry/instrumentation/otelzerolog/logger_test.go b/telemetry/instrumentation/otelzerolog/logger_test.go new file mode 100644 index 0000000..a1d932e --- /dev/null +++ b/telemetry/instrumentation/otelzerolog/logger_test.go @@ -0,0 +1,135 @@ +// Copyright 2024 CloudWeGo 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. + +package otelzerolog + +import ( + "bytes" + "context" + "testing" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + cwzerolog "github.com/cloudwego-contrib/cwgo-pkg/log/logging/zerolog" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func stdoutProvider(ctx context.Context) func() { + provider := sdktrace.NewTracerProvider() + otel.SetTracerProvider(provider) + + exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + panic(err) + } + + bsp := sdktrace.NewBatchSpanProcessor(exp) + provider.RegisterSpanProcessor(bsp) + + return func() { + if err := provider.Shutdown(ctx); err != nil { + panic(err) + } + } +} + +// TestLogger test logger work with opentelemetry +func TestLogger(t *testing.T) { + ctx := context.Background() + + buf := new(bytes.Buffer) + + shutdown := stdoutProvider(ctx) + defer shutdown() + + Zerologer := cwzerolog.New( + cwzerolog.WithOutput(buf), + cwzerolog.WithLevel(hlog.LevelDebug), + ) + logger := NewLogger( + WithLogger(Zerologer), + WithTraceErrorSpanLevel(zerolog.WarnLevel), + WithRecordStackTraceInSpan(true), + ) + + hlog.SetLogger(logger) + hlog.SetLevel(hlog.LevelDebug) + logger.Info("log from origin otelzerolog") + assert.Contains(t, buf.String(), "log from origin otelzerolog") + buf.Reset() + + tracer := otel.Tracer("test otel std logger") + + ctx, span := tracer.Start(ctx, "root") + + hlog.CtxInfof(ctx, "hello %s", "world") + assert.Contains(t, buf.String(), "trace_id") + assert.Contains(t, buf.String(), "span_id") + assert.Contains(t, buf.String(), "trace_flags") + buf.Reset() + + span.End() + + ctx, child1 := tracer.Start(ctx, "child1") + + hlog.CtxTracef(ctx, "trace %s", "this is a trace log") + hlog.CtxDebugf(ctx, "debug %s", "this is a debug log") + hlog.CtxInfof(ctx, "info %s", "this is a info log") + + child1.End() + assert.Equal(t, codes.Unset, child1.(sdktrace.ReadOnlySpan).Status().Code) + + ctx, child2 := tracer.Start(ctx, "child2") + hlog.CtxNoticef(ctx, "notice %s", "this is a notice log") + hlog.CtxWarnf(ctx, "warn %s", "this is a warn log") + hlog.CtxErrorf(ctx, "error %s", "this is a error log") + + child2.End() + assert.Equal(t, codes.Error, child2.(sdktrace.ReadOnlySpan).Status().Code) + + _, errSpan := tracer.Start(ctx, "error") + + hlog.Info("no trace context") + + errSpan.End() +} + +// TestLogLevel test SetLevel +func TestLogLevel(t *testing.T) { + buf := new(bytes.Buffer) + + logger := NewLogger( + WithTraceErrorSpanLevel(zerolog.WarnLevel), + WithRecordStackTraceInSpan(true), + ) + + logger.SetLevel(hlog.LevelError) + + // output to buffer + logger.SetOutput(buf) + + logger.Debug("this is a debug log") + assert.NotContains(t, buf.String(), "this is a debug log") + + logger.SetLevel(hlog.LevelDebug) + + logger.Debug("this is a debug log") + assert.Contains(t, buf.String(), "this is a debug log") +} diff --git a/telemetry/instrumentation/otelzerolog/option.go b/telemetry/instrumentation/otelzerolog/option.go new file mode 100644 index 0000000..bf22434 --- /dev/null +++ b/telemetry/instrumentation/otelzerolog/option.go @@ -0,0 +1,111 @@ +// Copyright 2024 CloudWeGo 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. + +package otelzerolog + +import ( + "errors" + + cwzerolog "github.com/cloudwego-contrib/cwgo-pkg/log/logging/zerolog" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type traceConfig struct { + recordStackTraceInSpan bool + errorSpanLevel zerolog.Level +} + +type config struct { + logger *cwzerolog.Logger + zeroLogger *zerolog.Logger + traceConfig *traceConfig +} + +// defaultConfig default config +func defaultConfig() *config { + return &config{ + traceConfig: &traceConfig{ + recordStackTraceInSpan: true, + errorSpanLevel: zerolog.ErrorLevel, + }, + logger: cwzerolog.New(), + } +} + +// WithZeroLogger configures logger +func WithZeroLogger(logger *zerolog.Logger) Option { + return option(func(cfg *config) { + cfg.zeroLogger = logger + }) +} + +// WithLogger configures zeroLogger +func WithLogger(logger *cwzerolog.Logger) Option { + return option(func(cfg *config) { + cfg.logger = logger + }) +} + +// WithTraceErrorSpanLevel trace error span level option +func WithTraceErrorSpanLevel(level zerolog.Level) Option { + return option(func(cfg *config) { + cfg.traceConfig.errorSpanLevel = level + }) +} + +// WithRecordStackTraceInSpan record stack track option +func WithRecordStackTraceInSpan(recordStackTraceInSpan bool) Option { + return option(func(cfg *config) { + cfg.traceConfig.recordStackTraceInSpan = recordStackTraceInSpan + }) +} + +func (cfg config) defaultZerologHookFn() zerolog.HookFunc { + return func(e *zerolog.Event, level zerolog.Level, message string) { + ctx := e.GetCtx() + span := trace.SpanFromContext(ctx) + spanCtx := span.SpanContext() + + if !spanCtx.IsValid() { + return + } + + e.Any(spanIDKey, spanCtx.SpanID()) + e.Any(traceIDKey, spanCtx.TraceID()) + e.Any(traceFlagsKey, spanCtx.TraceFlags()) + + if !span.IsRecording() { + return + } + + // set span status + if level >= cfg.traceConfig.errorSpanLevel { + span.SetStatus(codes.Error, "") + span.RecordError(errors.New(message), + trace.WithStackTrace(cfg.traceConfig.recordStackTraceInSpan)) + } + } +} diff --git a/telemetry/instrumentation/otelzerolog/utils.go b/telemetry/instrumentation/otelzerolog/utils.go new file mode 100644 index 0000000..f59fa64 --- /dev/null +++ b/telemetry/instrumentation/otelzerolog/utils.go @@ -0,0 +1,31 @@ +// Copyright 2024 CloudWeGo 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. + +package otelzerolog + +import ( + "strings" + + "github.com/rs/zerolog" +) + +// OtelSeverityText convert otelzerolog level to otel severityText +// ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#severity-fields +func OtelSeverityText(lv zerolog.Level) string { + s := strings.ToUpper(lv.String()) + if s == "PANIC" { + s = "FATAL" + } + return s +} diff --git a/telemetry/meter/global/measure.go b/telemetry/meter/global/measure.go new file mode 100644 index 0000000..6b88c3b --- /dev/null +++ b/telemetry/meter/global/measure.go @@ -0,0 +1,43 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package global + +import ( + "sync" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" +) + +// Global variable, used to store the current TracerProvider +var ( + tracerMeasure metric.Measure = metric.NewMeasure() + lock sync.Mutex +) + +// SetTracerMeasure used to set TracerProvider +func SetTracerMeasure(measure metric.Measure) { + lock.Lock() + defer lock.Unlock() + tracerMeasure = measure +} + +// GetTracerMeasure used to get TracerProvider +func GetTracerMeasure() metric.Measure { + lock.Lock() + defer lock.Unlock() + return tracerMeasure +} diff --git a/telemetry/meter/label/label.go b/telemetry/meter/label/label.go new file mode 100644 index 0000000..1760975 --- /dev/null +++ b/telemetry/meter/label/label.go @@ -0,0 +1,75 @@ +// Copyright 2022 CloudWeGo 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. + +package label + +import ( + "strings" + + prom "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/attribute" +) + +type CwLabel struct { + Key string + Value string +} + +func ToCwLabelsFromOtels(otelAttributes []attribute.KeyValue) []CwLabel { + cwLabels := make([]CwLabel, len(otelAttributes)) + for i, attr := range otelAttributes { + cwLabels[i] = CwLabel{ + Key: replaceDot(string(attr.Key)), + Value: attr.Value.AsString(), + } + } + return cwLabels +} + +func ToOtelsFromCwLabel(cwLabels []CwLabel) []attribute.KeyValue { + otelAttributes := make([]attribute.KeyValue, len(cwLabels)) + for i, label := range cwLabels { + otelAttributes[i] = attribute.String(replaceUnderscore(label.Key), label.Value) + } + return otelAttributes +} + +func ToCwLabelFromPromelabel(labels prom.Labels) []CwLabel { + cwLabels := make([]CwLabel, len(labels)) + i := 0 + for key, value := range labels { + cwLabels[i] = CwLabel{ + Key: key, + Value: value, + } + i++ + } + return cwLabels +} + +func ToPromelabelFromCwLabel(labels []CwLabel) prom.Labels { + promLabels := make(prom.Labels, len(labels)) + for _, label := range labels { + promLabels[label.Key] = label.Value + } + return promLabels +} + +func replaceUnderscore(input string) string { + return strings.ReplaceAll(input, "_", ".") +} + +func replaceDot(input string) string { + return strings.ReplaceAll(input, ".", "_") +} diff --git a/telemetry/meter/metric/measure.go b/telemetry/meter/metric/measure.go new file mode 100644 index 0000000..76027bd --- /dev/null +++ b/telemetry/meter/metric/measure.go @@ -0,0 +1,38 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package metric + +import ( + "context" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" +) + +type Measure interface { + Inc(ctx context.Context, metricType string, labels ...label.CwLabel) error + Add(ctx context.Context, metricType string, value int, labels ...label.CwLabel) error + Record(ctx context.Context, metricType string, value float64, labels ...label.CwLabel) error +} + +type Counter interface { + Inc(ctx context.Context, labels ...label.CwLabel) error + Add(ctx context.Context, value int, labels ...label.CwLabel) error +} + +type Recorder interface { + Record(ctx context.Context, value float64, labels ...label.CwLabel) error +} diff --git a/telemetry/meter/metric/measureImp.go b/telemetry/meter/metric/measureImp.go new file mode 100644 index 0000000..6cb2e7c --- /dev/null +++ b/telemetry/meter/metric/measureImp.go @@ -0,0 +1,51 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package metric + +import ( + "context" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" +) + +var _ Measure = &MeasureImpl{} + +type MeasureImpl struct { + recoders map[string]Recorder + counters map[string]Counter +} + +func NewMeasure(opts ...Option) Measure { + cfg := newConfig(opts) + return &MeasureImpl{ + counters: cfg.counter, + recoders: cfg.recoders, + } +} + +func (m *MeasureImpl) Inc(ctx context.Context, metricType string, labels ...label.CwLabel) error { + return m.counters[metricType].Inc(ctx, labels...) +} + +func (m *MeasureImpl) Add(ctx context.Context, metricType string, value int, labels ...label.CwLabel) error { + return m.counters[metricType].Add(ctx, value, labels...) +} + +// Record Recorder interface implementation +func (m *MeasureImpl) Record(ctx context.Context, metricType string, value float64, labels ...label.CwLabel) error { + return m.recoders[metricType].Record(ctx, value, labels...) +} diff --git a/telemetry/meter/metric/option.go b/telemetry/meter/metric/option.go new file mode 100644 index 0000000..7f7f768 --- /dev/null +++ b/telemetry/meter/metric/option.go @@ -0,0 +1,66 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package metric + +// Option opts for Measure +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + recoders map[string]Recorder + counter map[string]Counter +} + +func defaultConfig() *config { + return &config{ + counter: map[string]Counter{}, + recoders: map[string]Recorder{}, + } +} + +func newConfig(opts []Option) *config { + cfg := defaultConfig() + + for _, opt := range opts { + opt.apply(cfg) + } + + return cfg +} + +func WithCounter(name string, counter Counter) Option { + return option(func(cfg *config) { + if counter != nil { + cfg.counter[name] = counter + } + }) +} + +func WithRecorder(name string, recorder Recorder) Option { + return option(func(cfg *config) { + if recorder != nil { + cfg.recoders[name] = recorder + } + }) +} diff --git a/telemetry/meter/metric/otelmeasures.go b/telemetry/meter/metric/otelmeasures.go new file mode 100644 index 0000000..bab2a07 --- /dev/null +++ b/telemetry/meter/metric/otelmeasures.go @@ -0,0 +1,72 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package metric + +import ( + "context" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + "go.opentelemetry.io/otel/metric" +) + +var _ Counter = &OtelCounter{} + +type OtelCounter struct { + counter metric.Int64Counter +} + +func NewOtelCounter(counter metric.Int64Counter) Counter { + if counter == nil { + return nil + } + return OtelCounter{ + counter: counter, + } +} + +func (o OtelCounter) Inc(ctx context.Context, labels ...label.CwLabel) error { + otelLabel := label.ToOtelsFromCwLabel(labels) + o.counter.Add(ctx, 1, metric.WithAttributes(otelLabel...)) + return nil +} + +func (o OtelCounter) Add(ctx context.Context, value int, labels ...label.CwLabel) error { + otelLabel := label.ToOtelsFromCwLabel(labels) + o.counter.Add(ctx, int64(value), metric.WithAttributes(otelLabel...)) + return nil +} + +var _ Recorder = &OtelRecorder{} + +type OtelRecorder struct { + histogram metric.Float64Histogram +} + +func NewOtelRecorder(histogram metric.Float64Histogram) Recorder { + if histogram == nil { + return nil + } + return &OtelRecorder{ + histogram: histogram, + } +} + +func (o OtelRecorder) Record(ctx context.Context, value float64, labels ...label.CwLabel) error { + otelLabel := label.ToOtelsFromCwLabel(labels) + o.histogram.Record(ctx, value, metric.WithAttributes(otelLabel...)) + return nil +} diff --git a/telemetry/meter/metric/promemetric_test.go b/telemetry/meter/metric/promemetric_test.go new file mode 100644 index 0000000..19ae858 --- /dev/null +++ b/telemetry/meter/metric/promemetric_test.go @@ -0,0 +1,95 @@ +// Copyright 2023 CloudWeGo 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. + +package metric + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + prom "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stretchr/testify/assert" +) + +var defaultBuckets = []float64{5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000} + +func TestMetrics(t *testing.T) { + registry := prom.NewRegistry() + ctx := context.Background() + http.Handle("/metrics-demo", promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError})) + + go func() { + if err := http.ListenAndServe(":9090", nil); err != nil { + hlog.Fatalf("HERTZ: Unable to start a http server, err: %s", err.Error()) + } + }() + + counter := prom.NewCounterVec( + prom.CounterOpts{ + Name: "test_counter", + ConstLabels: prom.Labels{"service": "prometheus-test"}, + }, + []string{"test1", "test2"}, + ) + + registry.MustRegister(counter) + + histogram := prom.NewHistogramVec( + prom.HistogramOpts{ + Name: "test_histogram", + ConstLabels: prom.Labels{"service": "prometheus-test"}, + Buckets: defaultBuckets, + }, + []string{"test1", "test2"}, + ) + + registry.MustRegister(histogram) + + labels := prom.Labels{ + "test1": "abc", + "test2": "def", + } + cwlabels := label.ToCwLabelFromPromelabel(labels) + prommmetric := NewMeasure( + WithCounter(semantic.HTTPCounter, NewPromCounter(counter)), + WithRecorder(semantic.HTTPLatency, NewPromRecorder(histogram))) + assert.Nil(t, prommmetric.Add(ctx, semantic.HTTPCounter, 6, cwlabels...)) + assert.Nil(t, prommmetric.Record(ctx, semantic.HTTPLatency, float64(100*time.Millisecond.Microseconds()), cwlabels...)) + + res, err := http.Get("http://localhost:9090/metrics-demo") + + assert.Nil(t, err) + + defer res.Body.Close() + + bodyBytes, err := io.ReadAll(res.Body) + + assert.Nil(t, err) + + bodyStr := string(bodyBytes) + + assert.True(t, strings.Contains(bodyStr, `test_counter{service="prometheus-test",test1="abc",test2="def"} 6`)) + assert.True(t, strings.Contains(bodyStr, `test_histogram_bucket{service="prometheus-test",test1="abc",test2="def",le="50000"} 0`)) + assert.True(t, strings.Contains(bodyStr, `test_histogram_bucket{service="prometheus-test",test1="abc",test2="def",le="100000"} 1`)) +} diff --git a/telemetry/meter/metric/prommeasures.go b/telemetry/meter/metric/prommeasures.go new file mode 100644 index 0000000..0c6590f --- /dev/null +++ b/telemetry/meter/metric/prommeasures.go @@ -0,0 +1,78 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package metric + +import ( + "context" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + "github.com/prometheus/client_golang/prometheus" +) + +var _ Counter = &PromCounter{} + +type PromCounter struct { + counter *prometheus.CounterVec +} + +func NewPromCounter(counter *prometheus.CounterVec) *PromCounter { + return &PromCounter{ + counter: counter, + } +} + +func (p PromCounter) Inc(ctx context.Context, labels ...label.CwLabel) error { + pLabel := label.ToPromelabelFromCwLabel(labels) + counter, err := p.counter.GetMetricWith(pLabel) + if err != nil { + return err + } + counter.Add(float64(1)) + return nil +} + +func (p PromCounter) Add(ctx context.Context, value int, labels ...label.CwLabel) error { + pLabel := label.ToPromelabelFromCwLabel(labels) + counter, err := p.counter.GetMetricWith(pLabel) + if err != nil { + return err + } + counter.Add(float64(value)) + return nil +} + +var _ Recorder = &PromRecorder{} + +type PromRecorder struct { + histogram *prometheus.HistogramVec +} + +func NewPromRecorder(histogram *prometheus.HistogramVec) *PromRecorder { + return &PromRecorder{ + histogram: histogram, + } +} + +func (p PromRecorder) Record(ctx context.Context, value float64, labels ...label.CwLabel) error { + pLabel := label.ToPromelabelFromCwLabel(labels) + histogram, err := p.histogram.GetMetricWith(pLabel) + if err != nil { + return err + } + histogram.Observe(value) + return nil +} diff --git a/telemetry/provider/otelprovider/options.go b/telemetry/provider/otelprovider/options.go new file mode 100644 index 0000000..651e913 --- /dev/null +++ b/telemetry/provider/otelprovider/options.go @@ -0,0 +1,234 @@ +// Copyright 2022 CloudWeGo 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. + +package otelprovider + +import ( + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/contrib/propagators/ot" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" +) + +// Option opts for opentelemetry tracer provider +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + enableTracing bool + enableMetrics bool + + exportInsecure bool + exportEndpoint string + exportHeaders map[string]string + + resource *resource.Resource + sdkTracerProvider *sdktrace.TracerProvider + + sampler sdktrace.Sampler + + resourceAttributes []attribute.KeyValue + resourceDetectors []resource.Detector + + textMapPropagator propagation.TextMapPropagator + + meterProvider *metric.MeterProvider + + enableRPC bool + enableHTTP bool + + exportEnableCompression bool + + instanceType string +} + +func newConfig(opts []Option) *config { + cfg := defaultConfig() + + for _, opt := range opts { + opt.apply(cfg) + } + + return cfg +} + +func defaultConfig() *config { + return &config{ + enableTracing: true, + enableMetrics: true, + sampler: sdktrace.AlwaysSample(), + textMapPropagator: propagation.NewCompositeTextMapPropagator( + b3.New(), + ot.OT{}, + propagation.Baggage{}, + propagation.TraceContext{}, + ), + enableHTTP: false, + enableRPC: false, + } +} + +// WithServiceName configures `service.name` resource attribute +func WithServiceName(serviceName string) Option { + return option(func(cfg *config) { + cfg.resourceAttributes = append(cfg.resourceAttributes, semconv.ServiceNameKey.String(serviceName)) + }) +} + +// WithDeploymentEnvironment configures `deployment.environment` resource attribute +func WithDeploymentEnvironment(env string) Option { + return option(func(cfg *config) { + cfg.resourceAttributes = append(cfg.resourceAttributes, semconv.DeploymentEnvironmentKey.String(env)) + }) +} + +// WithServiceNamespace configures `service.namespace` resource attribute +func WithServiceNamespace(namespace string) Option { + return option(func(cfg *config) { + cfg.resourceAttributes = append(cfg.resourceAttributes, semconv.ServiceNamespaceKey.String(namespace)) + }) +} + +// WithResourceAttribute configures resource attribute +func WithResourceAttribute(rAttr attribute.KeyValue) Option { + return option(func(cfg *config) { + cfg.resourceAttributes = append(cfg.resourceAttributes, rAttr) + }) +} + +// WithResourceAttributes configures resource attributes. +func WithResourceAttributes(rAttrs []attribute.KeyValue) Option { + return option(func(cfg *config) { + cfg.resourceAttributes = rAttrs + }) +} + +// WithResource configures resource +func WithResource(resource *resource.Resource) Option { + return option(func(cfg *config) { + cfg.resource = resource + }) +} + +// WithExportEndpoint configures export endpoint +func WithExportEndpoint(endpoint string) Option { + return option(func(cfg *config) { + cfg.exportEndpoint = endpoint + }) +} + +// WithEnableTracing enable otelkitex +func WithEnableTracing(enableTracing bool) Option { + return option(func(cfg *config) { + cfg.enableTracing = enableTracing + }) +} + +// WithEnableMetrics enable meter +func WithEnableMetrics(enableMetrics bool) Option { + return option(func(cfg *config) { + cfg.enableMetrics = enableMetrics + }) +} + +// WithTextMapPropagator configures propagation +func WithTextMapPropagator(p propagation.TextMapPropagator) Option { + return option(func(cfg *config) { + cfg.textMapPropagator = p + }) +} + +// WithResourceDetector configures resource detector +func WithResourceDetector(detector resource.Detector) Option { + return option(func(cfg *config) { + cfg.resourceDetectors = append(cfg.resourceDetectors, detector) + }) +} + +// WithHeaders configures gRPC requests headers for exported telemetry data +func WithHeaders(headers map[string]string) Option { + return option(func(cfg *config) { + cfg.exportHeaders = headers + }) +} + +// WithInsecure disables client transport security for the exporter's gRPC +func WithInsecure() Option { + return option(func(cfg *config) { + cfg.exportInsecure = true + }) +} + +// WithSampler configures sampler +func WithSampler(sampler sdktrace.Sampler) Option { + return option(func(cfg *config) { + cfg.sampler = sampler + }) +} + +// WithSdkTracerProvider configures sdkTracerProvider +func WithSdkTracerProvider(sdkTracerProvider *sdktrace.TracerProvider) Option { + return option(func(cfg *config) { + cfg.sdkTracerProvider = sdkTracerProvider + }) +} + +// WithMeterProvider configures MeterProvider +func WithMeterProvider(meterProvider *metric.MeterProvider) Option { + return option(func(cfg *config) { + cfg.meterProvider = meterProvider + }) +} + +func WithHttpServer() Option { + return option(func(cfg *config) { + cfg.enableHTTP = true + }) +} + +func WithRPCServer() Option { + return option(func(cfg *config) { + cfg.enableRPC = true + }) +} + +func WithServer() Option { + return option(func(cfg *config) { + cfg.instanceType = "server" + }) +} + +func WithClient() Option { + return option(func(cfg *config) { + cfg.instanceType = "client" + }) +} + +// WithEnableCompression enable gzip transport compression +func WithEnableCompression() Option { + return option(func(cfg *config) { + cfg.exportEnableCompression = true + }) +} diff --git a/telemetry/provider/otelprovider/otelprovider.go b/telemetry/provider/otelprovider/otelprovider.go new file mode 100644 index 0000000..c709028 --- /dev/null +++ b/telemetry/provider/otelprovider/otelprovider.go @@ -0,0 +1,276 @@ +// Copyright 2022 CloudWeGo 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. + +package otelprovider + +import ( + "context" + "time" + + "github.com/cloudwego/kitex/pkg/klog" + + "github.com/cloudwego/hertz/pkg/common/hlog" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/global" + cwmetric "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider" + runtimemetrics "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +const ( + instrumentationNameKitex = "github.com/cloudwego-contrib/telemetry-opentelemetry/otelkitex" + instrumentationNameHertz = "github.com/cloudwego-contrib/telemetry-opentelemetry/otelhertz" +) + +var _ provider.Provider = &otelProvider{} + +type otelProvider struct { + traceExp *otlptrace.Exporter + metricsPusher *metric.MeterProvider +} + +func (p *otelProvider) Shutdown(ctx context.Context) error { + var err error + + if p.traceExp != nil { + if err = p.traceExp.Shutdown(ctx); err != nil { + otel.Handle(err) + } + } + + if p.metricsPusher != nil { + if err = p.metricsPusher.Shutdown(ctx); err != nil { + otel.Handle(err) + } + } + + return err +} + +// NewOpenTelemetryProvider Initializes an otlp trace and meter provider +func NewOpenTelemetryProvider(opts ...Option) provider.Provider { + var ( + err error + traceExp *otlptrace.Exporter + meterProvider *metric.MeterProvider + ) + + ctx := context.TODO() + + cfg := newConfig(opts) + + if !cfg.enableTracing && !cfg.enableMetrics { + return nil + } + + // resource + res := newResource(cfg) + + // propagator + otel.SetTextMapPropagator(cfg.textMapPropagator) + + // Tracing + if cfg.enableTracing { + // trace client + var traceClientOpts []otlptracegrpc.Option + if cfg.exportEndpoint != "" { + traceClientOpts = append(traceClientOpts, otlptracegrpc.WithEndpoint(cfg.exportEndpoint)) + } + if len(cfg.exportHeaders) > 0 { + traceClientOpts = append(traceClientOpts, otlptracegrpc.WithHeaders(cfg.exportHeaders)) + } + if cfg.exportInsecure { + traceClientOpts = append(traceClientOpts, otlptracegrpc.WithInsecure()) + } + if cfg.exportEnableCompression { + traceClientOpts = append(traceClientOpts, otlptracegrpc.WithCompressor("gzip")) + } + + traceClient := otlptracegrpc.NewClient(traceClientOpts...) + + // trace exporter + traceExp, err = otlptrace.New(ctx, traceClient) + if err != nil { + hlog.Fatalf("failed to create otlp trace exporter: %s", err) + return nil + } + + // trace processor + bsp := sdktrace.NewBatchSpanProcessor(traceExp) + + // trace provider + tracerProvider := cfg.sdkTracerProvider + if tracerProvider == nil { + tracerProvider = sdktrace.NewTracerProvider( + sdktrace.WithSampler(cfg.sampler), + sdktrace.WithResource(res), + sdktrace.WithSpanProcessor(bsp), + ) + } + + otel.SetTracerProvider(tracerProvider) + } + + // Metrics + if cfg.enableMetrics { + // prometheus only supports CumulativeTemporalitySelector + + var metricsClientOpts []otlpmetricgrpc.Option + if cfg.exportEndpoint != "" { + metricsClientOpts = append(metricsClientOpts, otlpmetricgrpc.WithEndpoint(cfg.exportEndpoint)) + } + if len(cfg.exportHeaders) > 0 { + metricsClientOpts = append(metricsClientOpts, otlpmetricgrpc.WithHeaders(cfg.exportHeaders)) + } + if cfg.exportInsecure { + metricsClientOpts = append(metricsClientOpts, otlpmetricgrpc.WithInsecure()) + } + if cfg.exportEnableCompression { + metricsClientOpts = append(metricsClientOpts, otlpmetricgrpc.WithCompressor("gzip")) + } + meterProvider = cfg.meterProvider + if meterProvider == nil { + // meter exporter + metricExp, err := otlpmetricgrpc.New(context.Background(), metricsClientOpts...) + if cfg.enableHTTP { + handleInitErrh(err, "Failed to create the metric exporter") + } + if cfg.enableRPC { + handleInitErrk(err, "Failed to create the metric exporter") + } + // reader := metric.NewPeriodicReader(exporter) + reader := metric.WithReader(metric.NewPeriodicReader(metricExp, metric.WithInterval(15*time.Second))) + + meterProvider = metric.NewMeterProvider(reader, metric.WithResource(res)) + } + + // meter pusher + otel.SetMeterProvider(meterProvider) + + var measure cwmetric.Measure + var metrics []cwmetric.Option + if cfg.enableRPC { + meter := meterProvider.Meter( + instrumentationNameKitex, + otelmetric.WithInstrumentationVersion(semantic.SemVersion()), + ) + serverRequestCountMeasure, err := meter.Int64Counter( + semantic.BuildMetricName("rpc", cfg.instanceType, semantic.RequestCount), + otelmetric.WithUnit("count"), + otelmetric.WithDescription("measures Incoming request count total"), + ) + HandleErr(err) + serverDurationMeasure, err := meter.Float64Histogram(semantic.BuildMetricName("rpc", cfg.instanceType, semantic.ServerDuration)) + HandleErr(err) + serverRetryMeasure, err := meter.Float64Histogram(semantic.BuildMetricName("rpc", cfg.instanceType, semantic.ServerRetry)) + HandleErr(err) + metrics = append(metrics, + cwmetric.WithCounter(semantic.RPCCounter, cwmetric.NewOtelCounter(serverRequestCountMeasure)), + cwmetric.WithRecorder(semantic.RPCLatency, cwmetric.NewOtelRecorder(serverDurationMeasure)), + cwmetric.WithRecorder(semantic.RPCRetry, cwmetric.NewOtelRecorder(serverRetryMeasure)), + ) + } + if cfg.enableHTTP { + meter := meterProvider.Meter( + instrumentationNameHertz, + otelmetric.WithInstrumentationVersion(semantic.SemVersion()), + ) + serverRequestCountMeasure, err := meter.Int64Counter( + semantic.BuildMetricName("http", cfg.instanceType, semantic.RequestCount), + otelmetric.WithUnit("count"), + otelmetric.WithDescription("measures Incoming request count total"), + ) + HandleErr(err) + + serverLatencyMeasure, err := meter.Float64Histogram( + semantic.BuildMetricName("http", cfg.instanceType, semantic.ServerLatency), + otelmetric.WithUnit("ms"), + otelmetric.WithDescription("measures th incoming end to end duration"), + ) + HandleErr(err) + metrics = append(metrics, + cwmetric.WithCounter(semantic.HTTPCounter, cwmetric.NewOtelCounter(serverRequestCountMeasure)), + cwmetric.WithRecorder(semantic.HTTPLatency, cwmetric.NewOtelRecorder(serverLatencyMeasure)), + ) + } + + measure = cwmetric.NewMeasure(metrics...) + + global.SetTracerMeasure(measure) + + err = runtimemetrics.Start() + if err != nil { + if cfg.enableHTTP { + handleInitErrh(err, "Failed to start runtime meter collector") + } + if cfg.enableRPC { + handleInitErrk(err, "Failed to start runtime meter collector") + } + } + + } + + return &otelProvider{ + traceExp: traceExp, + metricsPusher: meterProvider, + } +} + +func newResource(cfg *config) *resource.Resource { + if cfg.resource != nil { + return cfg.resource + } + + res, err := resource.New( + context.Background(), + resource.WithHost(), + resource.WithFromEnv(), + resource.WithProcessPID(), + resource.WithTelemetrySDK(), + resource.WithDetectors(cfg.resourceDetectors...), + resource.WithAttributes(cfg.resourceAttributes...), + ) + if err != nil { + return resource.Default() + } + return res +} + +func handleInitErrh(err error, message string) { + if err != nil { + hlog.Fatalf("%s: %v", message, err) + } +} + +func handleInitErrk(err error, message string) { + if err != nil { + klog.Fatalf("%s: %v", message, err) + } +} + +func HandleErr(err error) { + if err != nil { + otel.Handle(err) + } +} diff --git a/telemetry/provider/otelprovider/otelprovider_test.go b/telemetry/provider/otelprovider/otelprovider_test.go new file mode 100644 index 0000000..e97acb3 --- /dev/null +++ b/telemetry/provider/otelprovider/otelprovider_test.go @@ -0,0 +1,80 @@ +// Copyright 2022 CloudWeGo 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. + +package otelprovider + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" + semconv140 "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +func Test_newResource(t *testing.T) { + type args struct { + cfg *config + } + tests := []struct { + name string + args args + wantResources []attribute.KeyValue + unwantedResources []attribute.KeyValue + }{ + { + name: "with conflict schema version", + args: args{ + cfg: &config{ + resourceAttributes: []attribute.KeyValue{ + semconv140.ServiceNameKey.String("test-semconv-resource"), + }, + }, + }, + wantResources: []attribute.KeyValue{ + semconv.ServiceNameKey.String("test-semconv-resource"), + }, + unwantedResources: []attribute.KeyValue{ + semconv.ServiceNameKey.String("unknown_service:___Test_newResource_in_github_com_hertz_contrib_obs_opentelemetry_provider.test"), + }, + }, + { + name: "resource override", + args: args{ + cfg: &config{ + resource: resource.Default(), + resourceAttributes: []attribute.KeyValue{ + semconv.ServiceNameKey.String("test-resource"), + }, + }, + }, + wantResources: nil, + unwantedResources: []attribute.KeyValue{ + semconv.ServiceNameKey.String("test-resource"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := newResource(tt.args.cfg) + for _, res := range tt.wantResources { + assert.Contains(t, got.Attributes(), res) + } + for _, unwantedResource := range tt.unwantedResources { + assert.NotContains(t, got.Attributes(), unwantedResource) + } + }) + } +} diff --git a/telemetry/provider/promprovider/option.go b/telemetry/provider/promprovider/option.go new file mode 100644 index 0000000..3931796 --- /dev/null +++ b/telemetry/provider/promprovider/option.go @@ -0,0 +1,101 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package promprovider + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +var ( + defaultBuckets = []float64{5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000} + retryBuckets = []float64{0, 5, 10, 50, 100, 1000, 5000, 10000, 50000} +) + +// Option opts for opentelemetry tracer provider +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + buckets []float64 + registry *prometheus.Registry + + name string + enableRPC bool + enableHTTP bool +} + +func newConfig(opts []Option) *config { + cfg := defaultConfig() + + for _, opt := range opts { + opt.apply(cfg) + } + + return cfg +} + +func defaultConfig() *config { + return &config{ + buckets: defaultBuckets, + registry: prometheus.NewRegistry(), + enableHTTP: false, + enableRPC: false, + } +} + +// WithRegistry define your custom registry +func WithRegistry(registry *prometheus.Registry) Option { + return option(func(cfg *config) { + if registry != nil { + cfg.registry = registry + } + }) +} + +func WithHttpServer() Option { + return option(func(cfg *config) { + cfg.enableHTTP = true + }) +} + +func WithRPCServer() Option { + return option(func(cfg *config) { + cfg.enableRPC = true + }) +} + +// WithHistogramBuckets define your custom histogram buckets base on your biz +func WithHistogramBuckets(buckets []float64) Option { + return option(func(cfg *config) { + if len(buckets) > 0 { + cfg.buckets = buckets + } + }) +} + +func WithServiceName(name string) Option { + return option(func(cfg *config) { + cfg.name = name + }) +} diff --git a/telemetry/provider/promprovider/promprovider.go b/telemetry/provider/promprovider/promprovider.go new file mode 100644 index 0000000..72ff23e --- /dev/null +++ b/telemetry/provider/promprovider/promprovider.go @@ -0,0 +1,158 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package promprovider + +import ( + "context" + "fmt" + "net/http" + + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/global" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/metric" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + "github.com/prometheus/client_golang/prometheus" +) + +var _ provider.Provider = &promProvider{} + +// promProvider Structure of promProvider, including Prometheus registry and HTTP server +type promProvider struct { + registry *prometheus.Registry +} + +// Shutdown Implement the Shutdown method for the Provider interface +func (p *promProvider) Shutdown(ctx context.Context) error { + // close http server + return nil +} + +// NewPromProvider Initialize and return a new promProvider instance +func NewPromProvider(opts ...Option) *promProvider { + cfg := newConfig(opts) + registry := cfg.registry + if registry == nil { + registry = prometheus.NewRegistry() + } + var measure metric.Measure + var metrics []metric.Option + if cfg.enableRPC { + RPCCounterVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: buildName(cfg.name, "rpc", semantic.Counter), + Help: fmt.Sprintf("Total number of requires completed by the %s, regardless of success or failure.", semantic.Counter), + }, + []string{semantic.LabelRPCCallerKey, semantic.LabelRPCCalleeKey, semantic.LabelRPCMethodKey, semantic.LabelKeyStatus}, + ) + registry.MustRegister(RPCCounterVec) + counter := metric.NewPromCounter(RPCCounterVec) + + clientHandledHistogramRPC := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: buildName(cfg.name, "rpc", semantic.Latency), + Help: fmt.Sprintf("Latency (microseconds) of the %s until it is finished.", semantic.Latency), + Buckets: cfg.buckets, + }, + []string{semantic.LabelRPCCallerKey, semantic.LabelRPCCalleeKey, semantic.LabelRPCMethodKey, semantic.LabelKeyStatus}, + ) + registry.MustRegister(clientHandledHistogramRPC) + recorder := metric.NewPromRecorder(clientHandledHistogramRPC) + // create retry recorder + retryHandledHistogramRPC := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: buildName(cfg.name, "rpc", semantic.Retry), + Help: fmt.Sprintf("Distribution of retry attempts for %s until it is finished.", semantic.Retry), + Buckets: retryBuckets, + }, + []string{semantic.LabelRPCCallerKey, semantic.LabelRPCCalleeKey, semantic.LabelRPCMethodKey}, + ) + registry.MustRegister(retryHandledHistogramRPC) + retryRecorder := metric.NewPromRecorder(retryHandledHistogramRPC) + + metrics = append(metrics, + metric.WithCounter(semantic.RPCCounter, counter), + metric.WithRecorder(semantic.RPCLatency, recorder), + metric.WithRecorder(semantic.RPCRetry, retryRecorder), + ) + } + if cfg.enableHTTP { + HttpCounterVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: buildName(cfg.name, "http", semantic.Counter), + Help: "Total number of HTTPs completed by the server, regardless of success or failure.", + }, + []string{semantic.LabelHttpMethodKey, semantic.LabelStatusCode, semantic.LabelPath}, + ) + registry.MustRegister(HttpCounterVec) + counter := metric.NewPromCounter(HttpCounterVec) + + HttpHandledHistogram := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: buildName(cfg.name, "http", semantic.Latency), + Help: "Latency (microseconds) of HTTP that had been application-level handled by the server.", + Buckets: cfg.buckets, + }, + []string{semantic.LabelHttpMethodKey, semantic.LabelStatusCode, semantic.LabelPath}, + ) + registry.MustRegister(HttpHandledHistogram) + + recorder := metric.NewPromRecorder(HttpHandledHistogram) + + metrics = append(metrics, + metric.WithCounter(semantic.HTTPCounter, counter), + metric.WithRecorder(semantic.HTTPLatency, recorder), + ) + } + + measure = metric.NewMeasure(metrics...) + + global.SetTracerMeasure(measure) + + return &promProvider{ + registry: registry, + } +} + +func (p *promProvider) Serve(addr, path string) { + http.Handle(path, promhttp.HandlerFor(p.registry, promhttp.HandlerOpts{ + ErrorHandling: promhttp.ContinueOnError, + })) + go func() { + if err := http.ListenAndServe(addr, nil); err != nil { + hlog.Fatalf("HERTZ: Unable to start a http server, err: %s", err.Error()) + } + }() +} + +func buildName(name, protocol, service string) string { + if name != "" { + return fmt.Sprintf("%s_%s_%s", name, protocol, service) + } + return fmt.Sprintf("%s_%s", protocol, service) +} + +func Server(addr, path string, p provider.Provider) { + if promProv, ok := p.(*promProvider); ok { + promProv.Serve(addr, path) + } else { + hlog.Info("HERTZ: Server should put promProvider") + } +} diff --git a/telemetry/provider/provider.go b/telemetry/provider/provider.go new file mode 100644 index 0000000..3c438b8 --- /dev/null +++ b/telemetry/provider/provider.go @@ -0,0 +1,25 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package provider + +import ( + "context" +) + +type Provider interface { + Shutdown(ctx context.Context) error +} diff --git a/telemetry/provider/telemetryProvider/example/main.go b/telemetry/provider/telemetryProvider/example/main.go new file mode 100644 index 0000000..9192ef2 --- /dev/null +++ b/telemetry/provider/telemetryProvider/example/main.go @@ -0,0 +1,76 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/global" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/telemetryProvider" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/semantic" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/meter/label" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/promprovider" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + registry := prometheus.NewRegistry() + + provider := telemetryProvider.NewTelemetryProvider(telemetryProvider.WithProm( + promprovider.WithRegistry(registry), + promprovider.WithHttpServer()), + ) + defer provider.Shutdown(context.Background()) + + promprovider.Server(":9090", "/prometheus", provider) + + labels := []label.CwLabel{ + {Key: "http_method", Value: "/test"}, + {Key: "statusCode", Value: "200"}, + {Key: "path", Value: "/cwgo/provider/promProvider"}, + } + measure := global.GetTracerMeasure() + + // Simulate some processing + measure.Add(context.Background(), semantic.HTTPCounter, 6, labels...) + measure.Record(context.Background(), semantic.HTTPLatency, float64(time.Second.Microseconds()), labels...) + + promServerResp, err := http.Get("http://localhost:9090/prometheus") + if err != nil { + return + } + if promServerResp.StatusCode == http.StatusOK { + fmt.Print("status is 200\n") + } + + bodyBytes, err := io.ReadAll(promServerResp.Body) + if err != nil { + return + } + respStr := string(bodyBytes) + if strings.Contains(respStr, `counter{http_method="/test",path="/cwgo/provider/promProvider",statusCode="200"} 6`) && + strings.Contains(respStr, `latency_sum{http_method="/test",path="/cwgo/provider/promProvider",statusCode="200"} 1e+06`) { + fmt.Print("record and counter work correctly") + } +} diff --git a/telemetry/provider/telemetryProvider/option.go b/telemetry/provider/telemetryProvider/option.go new file mode 100644 index 0000000..6327621 --- /dev/null +++ b/telemetry/provider/telemetryProvider/option.go @@ -0,0 +1,60 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package telemetryProvider + +import ( + provider2 "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/otelprovider" + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider/promprovider" +) + +// Option opts for telemetry tracer provider +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type config struct { + provider provider2.Provider +} + +func WithOtel(opts ...otelprovider.Option) Option { + return option(func(cfg *config) { + cfg.provider = otelprovider.NewOpenTelemetryProvider(opts...) + }) +} + +func WithProm(opts ...promprovider.Option) Option { + return option(func(cfg *config) { + cfg.provider = promprovider.NewPromProvider(opts...) + }) +} + +func newConfig(opts []Option) *config { + cfg := &config{} + + for _, opt := range opts { + opt.apply(cfg) + } + + return cfg +} diff --git a/telemetry/provider/telemetryProvider/telemetry_provider.go b/telemetry/provider/telemetryProvider/telemetry_provider.go new file mode 100644 index 0000000..e98b90a --- /dev/null +++ b/telemetry/provider/telemetryProvider/telemetry_provider.go @@ -0,0 +1,39 @@ +/* + * Copyright 2024 CloudWeGo 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. + */ + +package telemetryProvider + +import ( + "context" + + "github.com/cloudwego-contrib/cwgo-pkg/telemetry/provider" +) + +var _ provider.Provider = &TelemetryProvider{} + +type TelemetryProvider struct { + provider provider.Provider +} + +func (t TelemetryProvider) Shutdown(ctx context.Context) error { + return t.provider.Shutdown(ctx) +} + +func NewTelemetryProvider(opts ...Option) provider.Provider { + cfg := newConfig(opts) + + return &TelemetryProvider{cfg.provider} +} diff --git a/telemetry/semantic/otel.go b/telemetry/semantic/otel.go new file mode 100644 index 0000000..cc19c29 --- /dev/null +++ b/telemetry/semantic/otel.go @@ -0,0 +1,104 @@ +// Copyright 2022 CloudWeGo 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. + +package semantic + +import ( + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" + oteltrace "go.opentelemetry.io/otel/trace" +) + +var ( + HTTPMetricsAttributes = []attribute.Key{ + semconv.HTTPHostKey, + semconv.HTTPRouteKey, + semconv.HTTPMethodKey, + semconv.HTTPStatusCodeKey, + } + // RPCMetricsAttributes rpc meter attributes + // ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#attributes + RPCMetricsAttributes = []attribute.Key{ + semconv.RPCServiceKey, + semconv.RPCSystemKey, + semconv.RPCMethodKey, + semconv.NetPeerNameKey, + semconv.NetTransportKey, + } + + PeerMetricsAttributes = []attribute.Key{ + semconv.PeerServiceKey, + PeerServiceNamespaceKey, + PeerDeploymentEnvironmentKey, + RequestProtocolKey, + SourceOperationKey, + } + + // MetricResourceAttributes resource attributes + MetricResourceAttributes = []attribute.Key{ + semconv.ServiceNameKey, + semconv.ServiceNamespaceKey, + semconv.DeploymentEnvironmentKey, + semconv.ServiceInstanceIDKey, + semconv.ServiceVersionKey, + semconv.TelemetrySDKLanguageKey, + semconv.TelemetrySDKVersionKey, + semconv.ProcessPIDKey, + semconv.HostNameKey, + semconv.HostIDKey, + } +) + +func ExtractMetricsAttributesFromSpan(span oteltrace.Span) []attribute.KeyValue { + var attrs []attribute.KeyValue + readOnlySpan, ok := span.(trace.ReadOnlySpan) + if !ok { + return attrs + } + + // span attributes + for _, attr := range readOnlySpan.Attributes() { + if matchAttributeKey(attr.Key, RPCMetricsAttributes) { + attrs = append(attrs, attr) + } + if matchAttributeKey(attr.Key, HTTPMetricsAttributes) { + attrs = append(attrs, attr) + } + if matchAttributeKey(attr.Key, PeerMetricsAttributes) { + attrs = append(attrs, attr) + } + } + + // span resource attributes + for _, attr := range readOnlySpan.Resource().Attributes() { + if matchAttributeKey(attr.Key, MetricResourceAttributes) { + attrs = append(attrs, attr) + } + } + + // status code + attrs = append(attrs, StatusKey.String(readOnlySpan.Status().Code.String())) + + return attrs +} + +func matchAttributeKey(key attribute.Key, toMatchKeys []attribute.Key) bool { + for _, attrKey := range toMatchKeys { + if attrKey == key { + return true + } + } + return false +} diff --git a/telemetry/semantic/semconv.go b/telemetry/semantic/semconv.go new file mode 100644 index 0000000..09f25ee --- /dev/null +++ b/telemetry/semantic/semconv.go @@ -0,0 +1,127 @@ +// Copyright 2022 CloudWeGo 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. + +package semantic + +import ( + "fmt" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.12.0" +) + +type ServiceType string + +// Keys for metrics +const ( + HTTPCounter = "httpCounter" + HTTPLatency = "httpLatency" + + RPCCounter = "rpcCounter" + RPCLatency = "rpcLatency" + RPCRetry = "rpcRetry" + + Counter = "counter" + Latency = "latency" + Retry = "retry" +) + +// RPC measure Labels +const ( + LabelRPCMethodKey = "rpc_method" + LabelRPCCalleeKey = "rpc_service" + LabelRPCCallerKey = "caller_rpc_service" + LabelKeyRetry = "retry" + LabelKeyStatus = "status" +) + +// HTTP measure Labels +const ( + LabelHttpMethodKey = "http_method" + LabelStatusCode = "http_status_code" + LabelPath = "path" +) + +// common Labels +const ( + LabelMethod = "method" + UnknownLabelValue = "unknown" + StatusSucceed = "succeed" + StatusError = "error" +) + +// otel keys +const ( + // RequestProtocolKey protocol of the request. + // + // Type: string + // Required: Always + // Examples: + // http: 'http' + // rpc: 'grpc', 'java_rmi', 'wcf', 'otelkitex' + // db: mysql, postgresql + // mq: 'rabbitmq', 'activemq', 'AmazonSQS' + RequestProtocolKey = attribute.Key("request.protocol") + + // RPCSystemKitexRecvSize recv_size + RPCSystemKitexRecvSize = attribute.Key("otelkitex.recv_size") + // RPCSystemKitexSendSize send_size + RPCSystemKitexSendSize = attribute.Key("otelkitex.send_size") + + // PeerServiceNamespaceKey peer.service.namespace + PeerServiceNamespaceKey = attribute.Key("peer.service.namespace") + // PeerDeploymentEnvironmentKey peer.deployment.environment + PeerDeploymentEnvironmentKey = attribute.Key("peer.deployment.environment") +) + +const ( + // SourceOperationKey source operation + // + // Type: string + // Required: Optional + // Examples: '/operation1' + SourceOperationKey = attribute.Key("source_operation") +) + +const ( + StatusKey = attribute.Key("status.code") +) + +// RPC Server meter +// ref to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/rpc.md#rpc-server +const ( + SeverThroughput = "throughput" + ServerDuration = "duration" // measures duration of inbound RPC + ServerRequestSize = "request.size" // measures size of RPC request messages (uncompressed) + ServerResponseSize = "response.size" // measures size of RPC response messages (uncompressed) + ServerRequestsPerRPC = "requests_per_rpc" // measures the number of messages received per RPC. Should be 1 for all non-streaming RPCs + ServerResponsesPerRPC = "responses_per_rpc" // measures the number of messages sent per RPC. Should be 1 for all non-streaming RPCs + ServerRetry = "retry" +) + +// Server HTTP meter +const ( + RequestCount = "request_count" // measures the incoming request count total + ServerLatency = "duration" // measures th incoming end to end duration +) + +// RPCSystemKitex Semantic convention for otelkitex as the remoting system. +var RPCSystemKitex = semconv.RPCSystemKey.String("kitex") + +func BuildMetricName(service, server, name string) string { + if server == "" { + return fmt.Sprintf("%s.%s", service, name) + } + return fmt.Sprintf("%s.%s.%s", service, server, name) +} diff --git a/telemetry/semantic/version.go b/telemetry/semantic/version.go new file mode 100644 index 0000000..ad9db2d --- /dev/null +++ b/telemetry/semantic/version.go @@ -0,0 +1,25 @@ +// Copyright 2022 CloudWeGo 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. + +package semantic + +// TODO move Version to semantic + +func Version() string { + return "0.39.0" +} + +func SemVersion() string { + return "semver:" + Version() +}