Repository: go-kratos/kratos
Pull request #3162 (2024-01-22 07:25:31)

fix(consul): unable to recreate after canceled the consul watch
<!--
🎉 Thanks for sending a pull request to Kratos! Here are some tips for you:

1. If this is your first time contributing to Kratos, please read our contribution guide: https://go-kratos.dev/en/docs/community/contribution/
2. Ensure you have added or ran the appropriate tests and lint for your PR, please use `make lint` and `make test` before filing your PR, use `make clean` to tidy your go mod.
3. If the PR is unfinished, you may need to mark it as a WIP(Work In Progress) PR or Draft PR
4. Please use a semantic commits format title, such as `<type>[optional scope]: <description>`, see: https://go-kratos.dev/docs/community/contribution#type
5. at the same time, please note that similar work should be submitted in one PR as far as possible to reduce the workload of reviewers. Do not split a work into multiple PR unless it should.
-->

<!--
🎉 感谢您向 Kratos 发送 PR！以下是一些提示：
如果这是你第一次为 Kratos 贡献，请阅读我们的贡献指南：https://go-kratos.dev/en/docs/community/contribution/
2、确保您已经为您的 PR 添加或运行了适当的测试和lint，请在提交PR之前使用“make lint”和“make test”，使用“make clean”整理您的 go.mod。
3、如果 PR 未完成，您可能需要将其标记为 WIP（Work In Progress）PR 或 Draft PR
4、请使用语义提交格式标题，如“<类型>[可选范围]：<说明>`，请参阅：https://go-kratos.dev/docs/community/contribution#type
5. 同时请注意，同类的工作请尽量在一个PR中提交，以减轻 review 者的工作负担，不要把一项工作拆分成很多个PR，除非它应该这样做。
-->


#### Description (what this PR does / why we need it):
<!--
* The description should include the motivation for this PR or contrast this with previous behavior
-->


#### Which issue(s) this PR fixes (resolves / be part of):
<!--
* Automatically closes linked issue when PR is merged.
* If your PR is not fully resolved the issue, please use `part of #<issue number>` instead.

Usage: `fixes/resolves #<issue number>`, or `fixes/resolves (paste link of issue)`.
-->


#### Other special notes for the reviewers:
<!--
* Somethings that need extra attention for the reviewers
* Some additional notes, TODO list, etc.
-->


harbourlga: agree
shenqidebaozi: Can it be fixed when multiple clients are created for the same service? For example, if a client is idle, will b client also be unable to update the instance due to Watcher shutdown?
harbourlga: > Can it be fixed when multiple clients are created for the same service? For example, if a client is idle, will b client also be unable to update the instance due to Watcher shutdown?
> 
> https://github.com/go-kratos/kratos/blob/0cb74ea2a330b1a91ca892a158a041d8925becad/contrib/registry/consul/watcher.go#L34

If you are talking about multiple grpc clients, then each client will build a different resolver to watch name resolution updates. Each resolver has its own context to control the shutdown of the Watcher

https://github.com/grpc/grpc-go/blob/02858ee5064040458f7e3e04e10765a62814c50b/resolver_wrapper.go#L80

https://github.com/go-kratos/kratos/blob/0cb74ea2a330b1a91ca892a158a041d8925becad/transport/grpc/resolver/discovery/builder.go#L85

https://github.com/go-kratos/kratos/blob/0cb74ea2a330b1a91ca892a158a041d8925becad/transport/grpc/resolver/discovery/builder.go#L88
harbourlga: > Can it be fixed when multiple clients are created for the same service? For example, if a client is idle, will b client also be unable to update the instance due to Watcher shutdown?
> 
> https://github.com/go-kratos/kratos/blob/0cb74ea2a330b1a91ca892a158a041d8925becad/contrib/registry/consul/watcher.go#L34

If you are talking about multiple grpc clients, then each client will build a different resolver to watch name resolution updates. Each resolver has its own context to control the shutdown of the Watcher. Therefore, when client a becomes idle, client b will not close the watcher.

When creating the grpc client, a resolver will be built: 
https://github.com/grpc/grpc-go/blob/02858ee5064040458f7e3e04e10765a62814c50b/resolver_wrapper.go#L80
https://github.com/go-kratos/kratos/blob/0cb74ea2a330b1a91ca892a158a041d8925becad/transport/grpc/resolver/discovery/builder.go#L86

https://github.com/go-kratos/kratos/blob/0cb74ea2a330b1a91ca892a158a041d8925becad/transport/grpc/resolver/discovery/builder.go#L88
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/go-kratos/kratos/pull/3162?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`a556a2b`)](https://app.codecov.io/gh/go-kratos/kratos/commit/a556a2b531202edc57f8b877b4d4ffbcc018ee6c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) 84.62% compared to head [(`238b5a5`)](https://app.codecov.io/gh/go-kratos/kratos/pull/3162?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) 84.62%.

> :exclamation: Current head 238b5a5 differs from pull request most recent head 2980638. Consider uploading reports for the commit 2980638 to get more accurate results


:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #3162   +/-   ##
=======================================
  Coverage   84.62%   84.62%           
=======================================
  Files          88       88           
  Lines        3993     3993           
=======================================
  Hits         3379     3379           
  Misses        440      440           
  Partials      174      174           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/go-kratos/kratos/pull/3162?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index b28a5a28..11fb5ea9 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -13,7 +13,7 @@ jobs:
   build:
     strategy:
       matrix:
-        go: [1.19,1.20.x,1.21.x]
+        go: [ 1.19.x, 1.20.x, 1.21.x, 1.22.x ]
     name: build & test
     runs-on: ubuntu-latest
     services:
@@ -54,7 +54,7 @@ jobs:
           echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
 
       - name: Module cache
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: |
             ~/.cache/go-build
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 225b90a9..09096550 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -29,7 +29,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Lint
-        uses: golangci/golangci-lint-action@v3
+        uses: golangci/golangci-lint-action@v4
         with:
           version: v1.54.2
           working-directory: ${{ matrix.workdir }}
diff --git a/contrib/metrics/prometheus/go.mod b/contrib/metrics/prometheus/go.mod
index ab3234b9..353d3153 100644
--- a/contrib/metrics/prometheus/go.mod
+++ b/contrib/metrics/prometheus/go.mod
@@ -4,18 +4,17 @@ go 1.19
 
 require (
 	github.com/go-kratos/kratos/v2 v2.7.2
-	github.com/prometheus/client_golang v1.17.0
-	github.com/prometheus/common v0.45.0
+	github.com/prometheus/client_golang v1.18.0
+	github.com/prometheus/common v0.46.0
 )
 
 require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
-	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
-	github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
-	github.com/prometheus/procfs v0.11.1 // indirect
-	golang.org/x/sys v0.13.0 // indirect
-	google.golang.org/protobuf v1.31.0 // indirect
+	github.com/prometheus/client_model v0.5.0 // indirect
+	github.com/prometheus/procfs v0.12.0 // indirect
+	golang.org/x/sys v0.16.0 // indirect
+	google.golang.org/protobuf v1.32.0 // indirect
 )
 
 replace github.com/go-kratos/kratos/v2 => ../../../
diff --git a/contrib/metrics/prometheus/go.sum b/contrib/metrics/prometheus/go.sum
index a15317e9..7a51e218 100644
--- a/contrib/metrics/prometheus/go.sum
+++ b/contrib/metrics/prometheus/go.sum
@@ -2,22 +2,16 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
-github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
-github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
-github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
-github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
-github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
-github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
-github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
-github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-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=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
+github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
+github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
diff --git a/contrib/opensergo/go.mod b/contrib/opensergo/go.mod
index 923f3e9c..136920cc 100644
--- a/contrib/opensergo/go.mod
+++ b/contrib/opensergo/go.mod
@@ -5,7 +5,7 @@ go 1.19
 require (
 	github.com/go-kratos/kratos/v2 v2.7.2
 	github.com/opensergo/opensergo-go v0.0.0-20220331070310-e5b01fee4d1c
-	golang.org/x/net v0.19.0
+	golang.org/x/net v0.20.0
 	google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d
 	google.golang.org/grpc v1.59.0
 	google.golang.org/protobuf v1.31.0
@@ -17,7 +17,7 @@ require (
 	github.com/google/uuid v1.3.1 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	golang.org/x/sync v0.3.0 // indirect
-	golang.org/x/sys v0.15.0 // indirect
+	golang.org/x/sys v0.16.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
diff --git a/contrib/opensergo/go.sum b/contrib/opensergo/go.sum
index 43215120..7abd8dde 100644
--- a/contrib/opensergo/go.sum
+++ b/contrib/opensergo/go.sum
@@ -51,7 +51,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
 github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -79,8 +79,8 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
-golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -93,8 +93,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
diff --git a/contrib/registry/consul/registry.go b/contrib/registry/consul/registry.go
index 8105ab1d..c83fa74c 100644
--- a/contrib/registry/consul/registry.go
+++ b/contrib/registry/consul/registry.go
@@ -190,6 +190,7 @@ func (r *Registry) Watch(ctx context.Context, name string) (registry.Watcher, er
 			services:    &atomic.Value{},
 			serviceName: name,
 		}
+		set.ctx, set.cancel = context.WithCancel(context.Background())
 		r.registry[name] = set
 	}
 
@@ -208,10 +209,8 @@ func (r *Registry) Watch(ctx context.Context, name string) (registry.Watcher, er
 		// otherwise the initial data may be blocked forever during the watch.
 		w.event <- struct{}{}
 	}
-
 	if !ok {
-		err := r.resolve(ctx, set)
-		if err != nil {
+		if err := r.resolve(set.ctx, set); err != nil {
 			return nil, err
 		}
 	}
@@ -249,6 +248,9 @@ func (r *Registry) resolve(ctx context.Context, ss *serviceSet) error {
 				}
 				idx = tmpIdx
 			case <-ctx.Done():
+				r.lock.Lock()
+				delete(r.registry, ss.serviceName)
+				r.lock.Unlock()
 				return
 			}
 		}
diff --git a/contrib/registry/consul/service.go b/contrib/registry/consul/service.go
index a0a16609..6567b772 100644
--- a/contrib/registry/consul/service.go
+++ b/contrib/registry/consul/service.go
@@ -1,6 +1,7 @@
 package consul
 
 import (
+	"context"
 	"sync"
 	"sync/atomic"
 
@@ -12,6 +13,9 @@ type serviceSet struct {
 	watcher     map[*watcher]struct{}
 	services    *atomic.Value
 	lock        sync.RWMutex
+
+	ctx    context.Context
+	cancel context.CancelFunc
 }
 
 func (s *serviceSet) broadcast(ss []*registry.ServiceInstance) {
diff --git a/contrib/registry/consul/watcher.go b/contrib/registry/consul/watcher.go
index 0a5d3575..f84caeb4 100644
--- a/contrib/registry/consul/watcher.go
+++ b/contrib/registry/consul/watcher.go
@@ -36,5 +36,9 @@ func (w *watcher) Stop() error {
 	w.set.lock.Lock()
 	defer w.set.lock.Unlock()
 	delete(w.set.watcher, w)
+	// close resolve
+	if len(w.set.watcher) == 0 {
+		w.set.cancel()
+	}
 	return nil
 }
diff --git a/contrib/registry/etcd/registry.go b/contrib/registry/etcd/registry.go
index ca858c99..476436e2 100644
--- a/contrib/registry/etcd/registry.go
+++ b/contrib/registry/etcd/registry.go
@@ -51,6 +51,11 @@ type Registry struct {
 	client *clientv3.Client
 	kv     clientv3.KV
 	lease  clientv3.Lease
+	/*
+		ctxMap is used to store the context cancel function of each service instance.
+		When the service instance is deregistered, the corresponding context cancel function is called to stop the heartbeat.
+	*/
+	ctxMap map[*registry.ServiceInstance]context.CancelFunc
 }
 
 // New creates etcd registry
@@ -68,6 +73,7 @@ func New(client *clientv3.Client, opts ...Option) (r *Registry) {
 		opts:   op,
 		client: client,
 		kv:     clientv3.NewKV(client),
+		ctxMap: make(map[*registry.ServiceInstance]context.CancelFunc),
 	}
 }
 
@@ -87,7 +93,9 @@ func (r *Registry) Register(ctx context.Context, service *registry.ServiceInstan
 		return err
 	}
 
-	go r.heartBeat(r.opts.ctx, leaseID, key, value)
+	hctx, cancel := context.WithCancel(r.opts.ctx)
+	r.ctxMap[service] = cancel
+	go r.heartBeat(hctx, leaseID, key, value)
 	return nil
 }
 
@@ -98,6 +106,11 @@ func (r *Registry) Deregister(ctx context.Context, service *registry.ServiceInst
 			r.lease.Close()
 		}
 	}()
+	// cancel heartbeat
+	if cancel, ok := r.ctxMap[service]; ok {
+		cancel()
+		delete(r.ctxMap, service)
+	}
 	key := fmt.Sprintf("%s/%s/%s", r.opts.namespace, service.Name, service.ID)
 	_, err := r.client.Delete(ctx, key)
 	return err
diff --git a/contrib/registry/nacos/watcher.go b/contrib/registry/nacos/watcher.go
index 78b4fc21..6fd59b37 100644
--- a/contrib/registry/nacos/watcher.go
+++ b/contrib/registry/nacos/watcher.go
@@ -14,14 +14,15 @@ import (
 var _ registry.Watcher = (*watcher)(nil)
 
 type watcher struct {
-	serviceName string
-	clusters    []string
-	groupName   string
-	ctx         context.Context
-	cancel      context.CancelFunc
-	watchChan   chan struct{}
-	cli         naming_client.INamingClient
-	kind        string
+	serviceName    string
+	clusters       []string
+	groupName      string
+	ctx            context.Context
+	cancel         context.CancelFunc
+	watchChan      chan struct{}
+	cli            naming_client.INamingClient
+	kind           string
+	subscribeParam *vo.SubscribeParam
 }
 
 func newWatcher(ctx context.Context, cli naming_client.INamingClient, serviceName, groupName, kind string, clusters []string) (*watcher, error) {
@@ -35,14 +36,15 @@ func newWatcher(ctx context.Context, cli naming_client.INamingClient, serviceNam
 	}
 	w.ctx, w.cancel = context.WithCancel(ctx)
 
-	e := w.cli.Subscribe(&vo.SubscribeParam{
+	w.subscribeParam = &vo.SubscribeParam{
 		ServiceName: serviceName,
 		Clusters:    clusters,
 		GroupName:   groupName,
 		SubscribeCallback: func(services []model.SubscribeService, err error) {
 			w.watchChan <- struct{}{}
 		},
-	})
+	}
+	e := w.cli.Subscribe(w.subscribeParam)
 	return w, e
 }
 
@@ -78,10 +80,7 @@ func (w *watcher) Next() ([]*registry.ServiceInstance, error) {
 }
 
 func (w *watcher) Stop() error {
+	err := w.cli.Unsubscribe(w.subscribeParam)
 	w.cancel()
-	return w.cli.Unsubscribe(&vo.SubscribeParam{
-		ServiceName: w.serviceName,
-		GroupName:   w.groupName,
-		Clusters:    w.clusters,
-	})
+	return err
 }
diff --git a/contrib/registry/polaris/registry.go b/contrib/registry/polaris/registry.go
index e968fe38..5ad520af 100644
--- a/contrib/registry/polaris/registry.go
+++ b/contrib/registry/polaris/registry.go
@@ -332,6 +332,7 @@ type Watcher struct {
 	Cancel           context.CancelFunc
 	Channel          <-chan model.SubScribeEvent
 	ServiceInstances []*registry.ServiceInstance
+	first            bool
 }
 
 func newWatcher(ctx context.Context, namespace string, serviceName string, consumer api.ConsumerAPI) (*Watcher, error) {
@@ -350,6 +351,7 @@ func newWatcher(ctx context.Context, namespace string, serviceName string, consu
 	w := &Watcher{
 		Namespace:        namespace,
 		ServiceName:      serviceName,
+		first:            true,
 		Channel:          watchServiceResponse.EventChannel,
 		ServiceInstances: instancesToServiceInstances(watchServiceResponse.GetAllInstancesResp.GetInstances()),
 	}
@@ -362,6 +364,10 @@ func newWatcher(ctx context.Context, namespace string, serviceName string, consu
 // 2.any service instance changes found.
 // if the above two conditions are not met, it will block until context deadline exceeded or canceled
 func (w *Watcher) Next() ([]*registry.ServiceInstance, error) {
+	if w.first {
+		w.first = false
+		return w.ServiceInstances, nil
+	}
 	select {
 	case <-w.Ctx.Done():
 		return nil, w.Ctx.Err()
diff --git a/contrib/registry/zookeeper/go.mod b/contrib/registry/zookeeper/go.mod
index 09997386..8a642beb 100644
--- a/contrib/registry/zookeeper/go.mod
+++ b/contrib/registry/zookeeper/go.mod
@@ -5,7 +5,7 @@ go 1.19
 require (
 	github.com/go-kratos/kratos/v2 v2.7.2
 	github.com/go-zookeeper/zk v1.0.3
-	golang.org/x/sync v0.5.0
+	golang.org/x/sync v0.6.0
 )
 
 replace github.com/go-kratos/kratos/v2 => ../../../
diff --git a/contrib/registry/zookeeper/go.sum b/contrib/registry/zookeeper/go.sum
index e9dcfb5b..a9c8f05b 100644
--- a/contrib/registry/zookeeper/go.sum
+++ b/contrib/registry/zookeeper/go.sum
@@ -1,4 +1,4 @@
 github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg=
 github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
diff --git a/encoding/form/proto_encode.go b/encoding/form/proto_encode.go
index 550648dc..584b1da5 100644
--- a/encoding/form/proto_encode.go
+++ b/encoding/form/proto_encode.go
@@ -111,7 +111,7 @@ func encodeRepeatedField(fieldDescriptor protoreflect.FieldDescriptor, list prot
 func encodeMapField(fieldDescriptor protoreflect.FieldDescriptor, mp protoreflect.Map) (map[string]string, error) {
 	m := make(map[string]string)
 	mp.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
-		key, err := EncodeField(fieldDescriptor.MapValue(), k.Value())
+		key, err := EncodeField(fieldDescriptor.MapKey(), k.Value())
 		if err != nil {
 			return false
 		}
diff --git a/log/helper.go b/log/helper.go
index cc0cb8c9..19fd613b 100644
--- a/log/helper.go
+++ b/log/helper.go
@@ -66,6 +66,15 @@ func (h *Helper) WithContext(ctx context.Context) *Helper {
 	}
 }
 
+// Enabled returns true if the given level above this level.
+// It delegates to the underlying *Filter.
+func (h *Helper) Enabled(level Level) bool {
+	if l, ok := h.logger.(*Filter); ok {
+		return level >= l.level
+	}
+	return true
+}
+
 // Log Print log by level and keyvals.
 func (h *Helper) Log(level Level, keyvals ...interface{}) {
 	_ = h.logger.Log(level, keyvals...)
@@ -73,11 +82,17 @@ func (h *Helper) Log(level Level, keyvals ...interface{}) {
 
 // Debug logs a message at debug level.
 func (h *Helper) Debug(a ...interface{}) {
+	if !h.Enabled(LevelDebug) {
+		return
+	}
 	_ = h.logger.Log(LevelDebug, h.msgKey, h.sprint(a...))
 }
 
 // Debugf logs a message at debug level.
 func (h *Helper) Debugf(format string, a ...interface{}) {
+	if !h.Enabled(LevelDebug) {
+		return
+	}
 	_ = h.logger.Log(LevelDebug, h.msgKey, h.sprintf(format, a...))
 }
 
@@ -88,11 +103,17 @@ func (h *Helper) Debugw(keyvals ...interface{}) {
 
 // Info logs a message at info level.
 func (h *Helper) Info(a ...interface{}) {
+	if !h.Enabled(LevelInfo) {
+		return
+	}
 	_ = h.logger.Log(LevelInfo, h.msgKey, h.sprint(a...))
 }
 
 // Infof logs a message at info level.
 func (h *Helper) Infof(format string, a ...interface{}) {
+	if !h.Enabled(LevelInfo) {
+		return
+	}
 	_ = h.logger.Log(LevelInfo, h.msgKey, h.sprintf(format, a...))
 }
 
@@ -103,11 +124,17 @@ func (h *Helper) Infow(keyvals ...interface{}) {
 
 // Warn logs a message at warn level.
 func (h *Helper) Warn(a ...interface{}) {
+	if !h.Enabled(LevelWarn) {
+		return
+	}
 	_ = h.logger.Log(LevelWarn, h.msgKey, h.sprint(a...))
 }
 
 // Warnf logs a message at warnf level.
 func (h *Helper) Warnf(format string, a ...interface{}) {
+	if !h.Enabled(LevelWarn) {
+		return
+	}
 	_ = h.logger.Log(LevelWarn, h.msgKey, h.sprintf(format, a...))
 }
 
@@ -118,11 +145,17 @@ func (h *Helper) Warnw(keyvals ...interface{}) {
 
 // Error logs a message at error level.
 func (h *Helper) Error(a ...interface{}) {
+	if !h.Enabled(LevelError) {
+		return
+	}
 	_ = h.logger.Log(LevelError, h.msgKey, h.sprint(a...))
 }
 
 // Errorf logs a message at error level.
 func (h *Helper) Errorf(format string, a ...interface{}) {
+	if !h.Enabled(LevelError) {
+		return
+	}
 	_ = h.logger.Log(LevelError, h.msgKey, h.sprintf(format, a...))
 }
 
diff --git a/log/std.go b/log/std.go
index 6f253981..24ee6acb 100644
--- a/log/std.go
+++ b/log/std.go
@@ -4,21 +4,25 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"log"
 	"sync"
 )
 
 var _ Logger = (*stdLogger)(nil)
 
+// stdLogger corresponds to the standard library's [log.Logger] and provides
+// similar capabilities. It also can be used concurrently by multiple goroutines.
 type stdLogger struct {
-	log  *log.Logger
-	pool *sync.Pool
+	w         io.Writer
+	isDiscard bool
+	mu        sync.Mutex
+	pool      *sync.Pool
 }
 
 // NewStdLogger new a logger with writer.
 func NewStdLogger(w io.Writer) Logger {
 	return &stdLogger{
-		log: log.New(w, "", 0),
+		w:         w,
+		isDiscard: w == io.Discard,
 		pool: &sync.Pool{
 			New: func() interface{} {
 				return new(bytes.Buffer)
@@ -29,21 +33,27 @@ func NewStdLogger(w io.Writer) Logger {
 
 // Log print the kv pairs log.
 func (l *stdLogger) Log(level Level, keyvals ...interface{}) error {
-	if len(keyvals) == 0 {
+	if l.isDiscard || len(keyvals) == 0 {
 		return nil
 	}
 	if (len(keyvals) & 1) == 1 {
 		keyvals = append(keyvals, "KEYVALS UNPAIRED")
 	}
+
 	buf := l.pool.Get().(*bytes.Buffer)
+	defer l.pool.Put(buf)
+
 	buf.WriteString(level.String())
 	for i := 0; i < len(keyvals); i += 2 {
 		_, _ = fmt.Fprintf(buf, " %s=%v", keyvals[i], keyvals[i+1])
 	}
-	_ = l.log.Output(4, buf.String()) //nolint:gomnd
-	buf.Reset()
-	l.pool.Put(buf)
-	return nil
+	buf.WriteByte('\n')
+	defer buf.Reset()
+
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	_, err := l.w.Write(buf.Bytes())
+	return err
 }
 
 func (l *stdLogger) Close() error {
diff --git a/middleware/logging/logging.go b/middleware/logging/logging.go
index 9573d198..8595750b 100644
--- a/middleware/logging/logging.go
+++ b/middleware/logging/logging.go
@@ -37,7 +37,7 @@ func Server(logger log.Logger) middleware.Middleware {
 				reason = se.Reason
 			}
 			level, stack := extractError(err)
-			_ = log.WithContext(ctx, logger).Log(level,
+			log.NewHelper(log.WithContext(ctx, logger)).Log(level,
 				"kind", "server",
 				"component", kind,
 				"operation", operation,
@@ -73,7 +73,7 @@ func Client(logger log.Logger) middleware.Middleware {
 				reason = se.Reason
 			}
 			level, stack := extractError(err)
-			_ = log.WithContext(ctx, logger).Log(level,
+			log.NewHelper(log.WithContext(ctx, logger)).Log(level,
 				"kind", "client",
 				"component", kind,
 				"operation", operation,


Test patch:
diff --git a/contrib/registry/consul/registry_test.go b/contrib/registry/consul/registry_test.go
index 02b61720..d6799b69 100644
--- a/contrib/registry/consul/registry_test.go
+++ b/contrib/registry/consul/registry_test.go
@@ -413,6 +413,226 @@ func TestRegistry_Watch(t *testing.T) {
 	}
 }
 
+func TestRegistry_IdleAndWatch(t *testing.T) {
+	addr := fmt.Sprintf("%s:9091", getIntranetIP())
+
+	time.Sleep(time.Millisecond * 100)
+	cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500", WaitTime: 2 * time.Second})
+	if err != nil {
+		t.Fatalf("create consul client failed: %v", err)
+	}
+
+	r := New(cli, []Option{
+		WithHealthCheck(false),
+	}...)
+
+	instance1 := &registry.ServiceInstance{
+		ID:        "1",
+		Name:      "server-1",
+		Version:   "v0.0.1",
+		Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)},
+	}
+
+	type args struct {
+		ctx      context.Context
+		instance *registry.ServiceInstance
+	}
+
+	tests := []struct {
+		name    string
+		args    args
+		want    []*registry.ServiceInstance
+		wantErr bool
+	}{
+		{
+			name: "many client, one idle",
+			args: args{
+				ctx:      context.Background(),
+				instance: instance1,
+			},
+			want:    []*registry.ServiceInstance{instance1},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err = r.Register(tt.args.ctx, tt.args.instance)
+			if err != nil {
+				t.Error(err)
+			}
+			defer func() {
+				err = r.Deregister(tt.args.ctx, tt.args.instance)
+				if err != nil {
+					t.Error(err)
+				}
+			}()
+
+			for i := 0; i < 10; i++ {
+				watch, err1 := r.Watch(context.Background(), tt.args.instance.Name)
+				if err1 != nil {
+					t.Error(err1)
+				}
+				go func(i int) {
+					// first
+					service, err2 := watch.Next()
+					if (err2 != nil) != tt.wantErr {
+						t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr)
+						t.Errorf("GetService() got = %v", service)
+						return
+					}
+					// instance changes
+					service, err2 = watch.Next()
+					if i == 9 {
+						return
+					}
+					if (err2 != nil) != tt.wantErr {
+						t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr)
+						t.Errorf("GetService() got = %v", service)
+						return
+					}
+					if !reflect.DeepEqual(service, tt.want) {
+						t.Errorf("GetService() got = %v, want %v", service, tt.want)
+					}
+					// t.Logf("service:%v, i:%d", service, i)
+				}(i)
+				if i == 9 {
+					time.Sleep(time.Second * 3)
+					// become idle, close watcher
+					err1 = watch.Stop()
+					if err1 != nil {
+						t.Errorf("watch stop err:%v", err)
+					}
+				}
+			}
+			time.Sleep(2 * time.Second)
+			change := tt.args.instance
+			change.Version = "v0.0.2"
+			err = r.Register(tt.args.ctx, change)
+			if err != nil {
+				t.Error(err)
+			}
+			time.Sleep(1 * time.Second)
+		})
+	}
+}
+
+func TestRegistry_IdleAndWatch2(t *testing.T) {
+	addr := fmt.Sprintf("%s:9091", getIntranetIP())
+
+	time.Sleep(time.Millisecond * 100)
+	cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500", WaitTime: 2 * time.Second})
+	if err != nil {
+		t.Fatalf("create consul client failed: %v", err)
+	}
+
+	instance1 := &registry.ServiceInstance{
+		ID:        "1",
+		Name:      "server-1",
+		Version:   "v0.0.1",
+		Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)},
+	}
+	type args struct {
+		ctx      context.Context
+		opts     []Option
+		instance *registry.ServiceInstance
+	}
+
+	tests := []struct {
+		name    string
+		args    args
+		want    []*registry.ServiceInstance
+		wantErr bool
+	}{
+		{
+			name: "all clients are idle, create a new one",
+			args: args{
+				ctx:      context.Background(),
+				instance: instance1,
+				opts: []Option{
+					WithHealthCheck(false),
+				},
+			},
+			want:    []*registry.ServiceInstance{instance1},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := New(cli, tt.args.opts...)
+
+			err = r.Register(tt.args.ctx, tt.args.instance)
+			if err != nil {
+				t.Error(err)
+			}
+			defer func() {
+				err = r.Deregister(tt.args.ctx, tt.args.instance)
+				if err != nil {
+					t.Error(err)
+				}
+			}()
+
+			ctx, cancel := context.WithCancel(context.Background())
+			for i := 0; i < 10; i++ {
+				stopCtx, stopCancel := context.WithCancel(ctx)
+				watch, err1 := r.Watch(context.Background(), tt.args.instance.Name)
+				if err1 != nil {
+					t.Error(err1)
+				}
+				go func(i int) {
+					// first
+					service, err2 := watch.Next()
+					if (err2 != nil) != tt.wantErr {
+						t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr)
+						t.Errorf("GetService() got = %v", service)
+						return
+					}
+					_, err2 = watch.Next()
+					if err2 == nil {
+						t.Errorf("watch exit exception:%d ", i)
+					}
+				}(i)
+				go func() {
+					select {
+					case <-stopCtx.Done():
+						err1 = watch.Stop()
+						if err1 != nil {
+							t.Errorf("watch stop err:%v", err)
+						}
+						return
+					case <-time.After(time.Minute):
+						stopCancel()
+						err1 = watch.Stop()
+						if err1 != nil {
+							t.Errorf("watch stop err:%v", err)
+						}
+						return
+					}
+				}()
+			}
+			time.Sleep(time.Second * 3)
+			cancel()
+			time.Sleep(time.Second * 2)
+			// Everything is idle. Add new watch.
+			watchCtx := context.Background()
+			watch, err := r.Watch(watchCtx, tt.args.instance.Name)
+			if err != nil {
+				t.Error(err)
+			}
+			service, err := watch.Next()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("GetService() got = %v", service)
+				return
+			}
+			if !reflect.DeepEqual(service, tt.want) {
+				t.Errorf("GetService() got = %v, want %v", service, tt.want)
+			}
+		})
+	}
+}
+
 func getIntranetIP() string {
 	addrs, err := net.InterfaceAddrs()
 	if err != nil {
diff --git a/log/helper_test.go b/log/helper_test.go
index c52f338b..750710c2 100644
--- a/log/helper_test.go
+++ b/log/helper_test.go
@@ -46,6 +46,13 @@ func BenchmarkHelperPrint(b *testing.B) {
 	}
 }
 
+func BenchmarkHelperPrintFilterLevel(b *testing.B) {
+	log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterLevel(LevelDebug)))
+	for i := 0; i < b.N; i++ {
+		log.Debug("test")
+	}
+}
+
 func BenchmarkHelperPrintf(b *testing.B) {
 	log := NewHelper(NewStdLogger(io.Discard))
 	for i := 0; i < b.N; i++ {
@@ -53,6 +60,13 @@ func BenchmarkHelperPrintf(b *testing.B) {
 	}
 }
 
+func BenchmarkHelperPrintfFilterLevel(b *testing.B) {
+	log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterLevel(LevelInfo)))
+	for i := 0; i < b.N; i++ {
+		log.Debugf("%s", "test")
+	}
+}
+
 func BenchmarkHelperPrintw(b *testing.B) {
 	log := NewHelper(NewStdLogger(io.Discard))
 	for i := 0; i < b.N; i++ {
diff --git a/log/std_test.go b/log/std_test.go
index 2518a046..971b169f 100644
--- a/log/std_test.go
+++ b/log/std_test.go
@@ -1,6 +1,11 @@
 package log
 
-import "testing"
+import (
+	"bytes"
+	"testing"
+
+	"golang.org/x/sync/errgroup"
+)
 
 func TestStdLogger(_ *testing.T) {
 	logger := DefaultLogger
@@ -15,3 +20,21 @@ func TestStdLogger(_ *testing.T) {
 	logger2 := DefaultLogger
 	_ = logger2.Log(LevelDebug)
 }
+
+func TestStdLogger_Log(t *testing.T) {
+	var b bytes.Buffer
+	logger := NewStdLogger(&b)
+
+	var eg errgroup.Group
+	eg.Go(func() error { return logger.Log(LevelInfo, "msg", "a", "k", "v") })
+	eg.Go(func() error { return logger.Log(LevelInfo, "msg", "a", "k", "v") })
+
+	err := eg.Wait()
+	if err != nil {
+		t.Fatalf("log error: %v", err)
+	}
+
+	if s := b.String(); s != "INFO msg=a k=v\nINFO msg=a k=v\n" {
+		t.Fatalf("log not match: %q", s)
+	}
+}
diff --git a/middleware/logging/logging_test.go b/middleware/logging/logging_test.go
index 76cad19b..1e949518 100644
--- a/middleware/logging/logging_test.go
+++ b/middleware/logging/logging_test.go
@@ -177,3 +177,59 @@ func TestExtractError(t *testing.T) {
 		})
 	}
 }
+
+type extractKeyValues [][]any
+
+func (l *extractKeyValues) Log(_ log.Level, kv ...any) error { *l = append(*l, kv); return nil }
+
+func TestServer_CallerPath(t *testing.T) {
+	var a extractKeyValues
+	logger := log.With(&a, "caller", log.Caller(5)) // report where the helper was called
+
+	// make sure the caller is same
+	sameCaller := func(fn middleware.Handler) { _, _ = fn(context.Background(), nil) }
+
+	// caller: [... log inside middleware, fn(context.Background(), nil)]
+	h := func(context.Context, any) (a any, e error) { return }
+	h = Server(logger)(h)
+	sameCaller(h)
+
+	// caller: [... helper.Info("foo"), fn(context.Background(), nil)]
+	helper := log.NewHelper(logger)
+	sameCaller(func(context.Context, any) (a any, e error) { helper.Info("foo"); return })
+
+	t.Log(a[0])
+	t.Log(a[1])
+	if a[0][0] != "caller" || a[1][0] != "caller" {
+		t.Fatal("caller not found")
+	}
+	if a[0][1] != a[1][1] {
+		t.Fatalf("middleware should have the same caller as log.Helper. middleware: %s, helper: %s", a[0][1], a[1][1])
+	}
+}
+
+func TestClient_CallerPath(t *testing.T) {
+	var a extractKeyValues
+	logger := log.With(&a, "caller", log.Caller(5)) // report where the helper was called
+
+	// make sure the caller is same
+	sameCaller := func(fn middleware.Handler) { _, _ = fn(context.Background(), nil) }
+
+	// caller: [... log inside middleware, fn(context.Background(), nil)]
+	h := func(context.Context, any) (a any, e error) { return }
+	h = Client(logger)(h)
+	sameCaller(h)
+
+	// caller: [... helper.Info("foo"), fn(context.Background(), nil)]
+	helper := log.NewHelper(logger)
+	sameCaller(func(context.Context, any) (a any, e error) { helper.Info("foo"); return })
+
+	t.Log(a[0])
+	t.Log(a[1])
+	if a[0][0] != "caller" || a[1][0] != "caller" {
+		t.Fatal("caller not found")
+	}
+	if a[0][1] != a[1][1] {
+		t.Fatalf("middleware should have the same caller as log.Helper. middleware: %s, helper: %s", a[0][1], a[1][1])
+	}
+}
diff --git a/transport/http/binding/encode_test.go b/transport/http/binding/encode_test.go
index 4236abf3..066e0624 100644
--- a/transport/http/binding/encode_test.go
+++ b/transport/http/binding/encode_test.go
@@ -33,12 +33,6 @@ func TestEncodeURL(t *testing.T) {
 			needQuery:    false,
 			want:         "http://helloworld.Greeter/helloworld/{}/sub/hello",
 		},
-		{
-			pathTemplate: "http://helloworld.Greeter/helloworld/{}/sub/{sub.naming}",
-			request:      &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "hello"}},
-			needQuery:    false,
-			want:         "http://helloworld.Greeter/helloworld/{}/sub/hello",
-		},
 		{
 			pathTemplate: "http://helloworld.Greeter/helloworld/{}/sub/{sub.name.cc}",
 			request:      &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "hello"}},
@@ -69,12 +63,6 @@ func TestEncodeURL(t *testing.T) {
 			needQuery:    false,
 			want:         "http://helloworld.Greeter/helloworld/test/sub/",
 		},
-		{
-			pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.name}",
-			request:      &binding.HelloRequest{Name: "test"},
-			needQuery:    false,
-			want:         "http://helloworld.Greeter/helloworld/test/sub/",
-		},
 		{
 			pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub",
 			request:      &binding.HelloRequest{Name: "go", Sub: &binding.Sub{Name: "kratos"}},
@@ -132,8 +120,8 @@ func TestEncodeURL(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		if EncodeURL(test.pathTemplate, test.request, test.needQuery) != test.want {
-			t.Fatalf("want: %s, got: %s", test.want, EncodeURL(test.pathTemplate, test.request, test.needQuery))
+		if path := EncodeURL(test.pathTemplate, test.request, test.needQuery); path != test.want {
+			t.Fatalf("want: %s, got: %s", test.want, path)
 		}
 	}
 }