Repository: go-kratos/kratos
Pull request #1620 (2021-11-10 09:20:06)

fix:add consul all health check
<!--
🎉 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 mark it as a WIP(Work In Progress) PR or draft PR
4. Please use a conventional commits format title: `<type>[optional scope]: <description>`
    
    Some suggestion on <type>:

    fix: A bug fix
    feat: A new feature
    test: Adding missing tests or correcting existing tests
    refactor: A code change that neither fixes a bug nor adds a feature
    break: Changes has break change

    docs: Documentation only changes
    deps: Changes external dependencies
    style: Changes that do not affect the meaning of the code (white-space, formatting, etc)
    chore Daily work, examples, etc.
    ci: Changes to our CI configuration files and scripts
-->

#### 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 you PR is not fully resolved issue, please use `part of #<issue number>` instead.

Usage: `fixes/resolves #<issue number>`, or `fixes/resolves (paste link of issue)`.
-->
fixes #

#### Other special notes for reviewer:
<!--
* Somethings that need extra attention for reviewer
* Some additional notes, TODO list, etc.
-->


codecov-commenter: # [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/1620?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
> Merging [#1620](https://codecov.io/gh/go-kratos/kratos/pull/1620?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (533079d) into [main](https://codecov.io/gh/go-kratos/kratos/commit/fcd935171749bc3117d8f9403cf715fa66018354?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (fcd9351) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/go-kratos/kratos/pull/1620/graphs/tree.svg?width=650&height=150&src=pr&token=XwnmBTz78x&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)](https://codecov.io/gh/go-kratos/kratos/pull/1620?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)

```diff
@@           Coverage Diff           @@
##             main    #1620   +/-   ##
=======================================
  Coverage   81.50%   81.50%           
=======================================
  Files          78       78           
  Lines        3412     3412           
=======================================
  Hits         2781     2781           
  Misses        432      432           
  Partials      199      199           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/go-kratos/kratos/pull/1620?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/1620?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Last update [fcd9351...533079d](https://codecov.io/gh/go-kratos/kratos/pull/1620?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).


Patch:
diff --git a/api/metadata/server.go b/api/metadata/server.go
index 00fdcb1b..50970a13 100644
--- a/api/metadata/server.go
+++ b/api/metadata/server.go
@@ -5,7 +5,7 @@ import (
 	"compress/gzip"
 	"context"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"sync"
 
 	"google.golang.org/grpc"
@@ -180,7 +180,7 @@ func decompress(b []byte) ([]byte, error) {
 	if err != nil {
 		return nil, fmt.Errorf("bad gzipped descriptor: %v", err)
 	}
-	out, err := ioutil.ReadAll(r)
+	out, err := io.ReadAll(r)
 	if err != nil {
 		return nil, fmt.Errorf("bad gzipped descriptor: %v", err)
 	}
diff --git a/cmd/kratos/internal/base/mod.go b/cmd/kratos/internal/base/mod.go
index 178a3f6d..2349aca9 100644
--- a/cmd/kratos/internal/base/mod.go
+++ b/cmd/kratos/internal/base/mod.go
@@ -3,7 +3,7 @@ package base
 import (
 	"bufio"
 	"bytes"
-	"io/ioutil"
+	"os"
 	"os/exec"
 	"path/filepath"
 	"strings"
@@ -13,7 +13,7 @@ import (
 
 // ModulePath returns go module path.
 func ModulePath(filename string) (string, error) {
-	modBytes, err := ioutil.ReadFile(filename)
+	modBytes, err := os.ReadFile(filename)
 	if err != nil {
 		return "", err
 	}
diff --git a/cmd/kratos/internal/base/path.go b/cmd/kratos/internal/base/path.go
index 62df6f48..6aac060c 100644
--- a/cmd/kratos/internal/base/path.go
+++ b/cmd/kratos/internal/base/path.go
@@ -3,7 +3,6 @@ package base
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
 	"log"
 	"os"
 	"path"
@@ -43,7 +42,7 @@ func copyFile(src, dst string, replaces []string) error {
 	if err != nil {
 		return err
 	}
-	buf, err := ioutil.ReadFile(src)
+	buf, err := os.ReadFile(src)
 	if err != nil {
 		return err
 	}
@@ -55,12 +54,12 @@ func copyFile(src, dst string, replaces []string) error {
 		}
 		buf = bytes.ReplaceAll(buf, []byte(old), []byte(next))
 	}
-	return ioutil.WriteFile(dst, buf, srcinfo.Mode())
+	return os.WriteFile(dst, buf, srcinfo.Mode())
 }
 
 func copyDir(src, dst string, replaces, ignores []string) error {
 	var err error
-	var fds []os.FileInfo
+	var fds []os.DirEntry
 	var srcinfo os.FileInfo
 
 	if srcinfo, err = os.Stat(src); err != nil {
@@ -71,7 +70,7 @@ func copyDir(src, dst string, replaces, ignores []string) error {
 		return err
 	}
 
-	if fds, err = ioutil.ReadDir(src); err != nil {
+	if fds, err = os.ReadDir(src); err != nil {
 		return err
 	}
 	for _, fd := range fds {
diff --git a/cmd/kratos/internal/change/get.go b/cmd/kratos/internal/change/get.go
index a5432834..b8c268bd 100644
--- a/cmd/kratos/internal/change/get.go
+++ b/cmd/kratos/internal/change/get.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"os"
 	"regexp"
@@ -104,7 +103,7 @@ func requestGithubAPI(url string, method string, body io.Reader, token string) (
 		fatal(err)
 	}
 	defer resp.Body.Close()
-	resBody, err := ioutil.ReadAll(resp.Body)
+	resBody, err := io.ReadAll(resp.Body)
 	if err != nil {
 		fatal(err)
 	}
diff --git a/cmd/kratos/internal/project/project.go b/cmd/kratos/internal/project/project.go
index dd0a3c18..0abc4577 100644
--- a/cmd/kratos/internal/project/project.go
+++ b/cmd/kratos/internal/project/project.go
@@ -70,11 +70,11 @@ func run(cmd *cobra.Command, args []string) {
 		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
 			fmt.Fprint(os.Stderr, "\033[31mERROR: project creation timed out\033[m\n")
 		} else {
-			fmt.Fprintf(os.Stderr, "\033[31mERROR: failed to create project(%s)\033[m\n", ctx.Err())
+			fmt.Fprintf(os.Stderr, "\033[31mERROR: failed to create project(%s)\033[m\n", ctx.Err().Error())
 		}
 	case err = <-done:
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "\033[31mERROR: Failed to create project(%s)\033[m\n", ctx.Err())
+			fmt.Fprintf(os.Stderr, "\033[31mERROR: Failed to create project(%s)\033[m\n", err.Error())
 		}
 	}
 }
diff --git a/cmd/kratos/internal/proto/add/add.go b/cmd/kratos/internal/proto/add/add.go
index c658aba2..2d0d6572 100644
--- a/cmd/kratos/internal/proto/add/add.go
+++ b/cmd/kratos/internal/proto/add/add.go
@@ -2,7 +2,7 @@ package add
 
 import (
 	"fmt"
-	"io/ioutil"
+	"os"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -44,9 +44,9 @@ func run(cmd *cobra.Command, args []string) {
 }
 
 func modName() string {
-	modBytes, err := ioutil.ReadFile("go.mod")
+	modBytes, err := os.ReadFile("go.mod")
 	if err != nil {
-		if modBytes, err = ioutil.ReadFile("../go.mod"); err != nil {
+		if modBytes, err = os.ReadFile("../go.mod"); err != nil {
 			return ""
 		}
 	}
diff --git a/cmd/kratos/internal/proto/add/proto.go b/cmd/kratos/internal/proto/add/proto.go
index e93e7087..8c716dc6 100644
--- a/cmd/kratos/internal/proto/add/proto.go
+++ b/cmd/kratos/internal/proto/add/proto.go
@@ -2,7 +2,6 @@ package add
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path"
 )
@@ -37,5 +36,5 @@ func (p *Proto) Generate() error {
 	if _, err := os.Stat(name); !os.IsNotExist(err) {
 		return fmt.Errorf("%s already exists", p.Name)
 	}
-	return ioutil.WriteFile(name, body, 0o644)
+	return os.WriteFile(name, body, 0o644)
 }
diff --git a/cmd/kratos/internal/proto/client/client.go b/cmd/kratos/internal/proto/client/client.go
index 62e4e446..8ac1743e 100644
--- a/cmd/kratos/internal/proto/client/client.go
+++ b/cmd/kratos/internal/proto/client/client.go
@@ -2,7 +2,6 @@ package client
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -98,7 +97,7 @@ func generate(proto string, args []string) error {
 		"--go-errors_out=paths=source_relative:.",
 	}
 	input = append(input, inputExt...)
-	protoBytes, err := ioutil.ReadFile(proto)
+	protoBytes, err := os.ReadFile(proto)
 	if err == nil && len(protoBytes) > 0 {
 		if ok, _ := regexp.Match(`\n[^/]*(import)\s+"validate/validate.proto"`, protoBytes); ok {
 			input = append(input, "--validate_out=lang=go,paths=source_relative:.")
diff --git a/cmd/kratos/internal/proto/server/server.go b/cmd/kratos/internal/proto/server/server.go
index 25dc289d..ea1d6e45 100644
--- a/cmd/kratos/internal/proto/server/server.go
+++ b/cmd/kratos/internal/proto/server/server.go
@@ -2,7 +2,6 @@ package server
 
 import (
 	"fmt"
-	"io/ioutil"
 	"log"
 	"os"
 	"path"
@@ -83,7 +82,7 @@ func run(cmd *cobra.Command, args []string) {
 		if err != nil {
 			log.Fatal(err)
 		}
-		if err := ioutil.WriteFile(to, b, 0o644); err != nil {
+		if err := os.WriteFile(to, b, 0o644); err != nil {
 			log.Fatal(err)
 		}
 		fmt.Println(to)
diff --git a/cmd/kratos/internal/run/run.go b/cmd/kratos/internal/run/run.go
index ce5f17f7..53ef7abc 100644
--- a/cmd/kratos/internal/run/run.go
+++ b/cmd/kratos/internal/run/run.go
@@ -2,7 +2,6 @@ package run
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"os/exec"
 	"path"
@@ -79,7 +78,7 @@ func findCMD(base string) (map[string]string, error) {
 		err := filepath.Walk(dir, func(walkPath string, info os.FileInfo, err error) error {
 			// multi level directory is not allowed under the cmdPath directory, so it is judged that the path ends with cmdPath.
 			if strings.HasSuffix(walkPath, "cmd") {
-				paths, err := ioutil.ReadDir(walkPath)
+				paths, err := os.ReadDir(walkPath)
 				if err != nil {
 					return err
 				}
diff --git a/cmd/protoc-gen-go-errors/errors.go b/cmd/protoc-gen-go-errors/errors.go
index 455f91a7..684310b9 100644
--- a/cmd/protoc-gen-go-errors/errors.go
+++ b/cmd/protoc-gen-go-errors/errors.go
@@ -94,7 +94,7 @@ func genErrorsReason(gen *protogen.Plugin, file *protogen.File, g *protogen.Gene
 
 func case2Camel(name string) string {
 	if !strings.Contains(name, "_") {
-		return name
+		return strings.Title(strings.ToLower(name))
 	}
 	name = strings.ToLower(name)
 	name = strings.Replace(name, "_", " ", -1)
diff --git a/cmd/protoc-gen-go-http/template.go b/cmd/protoc-gen-go-http/template.go
index 2c34647b..50671e57 100644
--- a/cmd/protoc-gen-go-http/template.go
+++ b/cmd/protoc-gen-go-http/template.go
@@ -30,6 +30,12 @@ func _{{$svrType}}_{{.Name}}{{.Num}}_HTTP_Handler(srv {{$svrType}}HTTPServer) fu
 		if err := ctx.Bind(&in{{.Body}}); err != nil {
 			return err
 		}
+		
+		{{- if not (eq .Body "")}}
+		if err := ctx.BindQuery(&in); err != nil {
+			return err
+		}
+		{{- end}}
 		{{- else}}
 		if err := ctx.BindQuery(&in{{.Body}}); err != nil {
 			return err
diff --git a/config/config.go b/config/config.go
index c3ce750b..5fed286b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -103,6 +103,9 @@ func (c *config) Load() error {
 		if err != nil {
 			return err
 		}
+		for _, v := range kvs {
+			c.log.Infof("config loaded: %s format: %s", v.Key, v.Format)
+		}
 		if err = c.reader.Merge(kvs...); err != nil {
 			c.log.Errorf("failed to merge config source: %v", err)
 			return err
diff --git a/config/file/file.go b/config/file/file.go
index 9a918c08..34a26734 100644
--- a/config/file/file.go
+++ b/config/file/file.go
@@ -1,7 +1,7 @@
 package file
 
 import (
-	"io/ioutil"
+	"io"
 	"os"
 	"path/filepath"
 	"strings"
@@ -26,7 +26,7 @@ func (f *file) loadFile(path string) (*config.KeyValue, error) {
 		return nil, err
 	}
 	defer file.Close()
-	data, err := ioutil.ReadAll(file)
+	data, err := io.ReadAll(file)
 	if err != nil {
 		return nil, err
 	}
@@ -42,7 +42,7 @@ func (f *file) loadFile(path string) (*config.KeyValue, error) {
 }
 
 func (f *file) loadDir(path string) (kvs []*config.KeyValue, err error) {
-	files, err := ioutil.ReadDir(f.path)
+	files, err := os.ReadDir(f.path)
 	if err != nil {
 		return nil, err
 	}
diff --git a/contrib/config/apollo/README.md b/contrib/config/apollo/README.md
new file mode 100644
index 00000000..c07b67ec
--- /dev/null
+++ b/contrib/config/apollo/README.md
@@ -0,0 +1,104 @@
+## Apollo config centry
+
+This module implements the `config.Source` interface in kratos based apollo config management center.
+
+[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/go-kratos/kratos/contrib/config/apollo/v2)
+
+### Quick start
+
+```go
+import (
+	"fmt"
+	"log"
+
+	"github.com/go-kratos/kratos/contrib/config/apollo/v2"
+	"github.com/go-kratos/kratos/v2/config"
+)
+
+func main() {
+	c := config.New(
+		config.WithSource(
+			apollo.NewSource(
+				apollo.WithAppID("kratos"),
+				apollo.WithCluster("dev"),
+				apollo.WithEndpoint("http://localhost:8080"),
+				apollo.WithNamespace("application,event.yaml,demo.json"),
+				apollo.WithEnableBackup(),
+				apollo.WithSecret("ad75b33c77ae4b9c9626d969c44f41ee"),
+			),
+		),
+	)
+	var bc bootstrap
+	if err := c.Load(); err != nil {
+		panic(err)
+	}
+	
+	// use value and watch operations，help yourself. 
+}
+```
+
+### Options list
+
+> You get what you see.
+
+```go
+// specify the app id
+func WithAppID(appID string) Option
+// specify the cluster of application
+func WithCluster(cluster string) Option
+
+// enable backup or not, and where to back up them.
+func WithBackupPath(backupPath string) Option
+func WithDisableBackup() Option
+func WithEnableBackup() Option
+
+// specify apollo endpoint, such as http://localhost:8080
+func WithEndpoint(endpoint string) Option
+
+// inject a logger to debug
+func WithLogger(logger log.Logger) Option
+
+// namespaces to load, comma to separate. 
+func WithNamespace(name string) Option
+
+// secret is the apollo secret key to access application config.
+func WithSecret(secret string) Option
+```
+
+### Notice
+
+apollo config center use `Namespace` to be part of the key. For example:
+
+***application.json***
+
+```json
+{
+  "http": {
+    "address": ":8080",
+    "tls": {
+      "enable": false,
+      "cert_file": "",
+      "key_file": ""
+    }
+  }
+}
+```
+
+you got them in kratos config instance maybe look like:
+
+```go
+config := map[string]interface{}{
+	// application be part of the key path.
+	"application": map[string]interface{}{
+        "http": map[string]interface{}{
+            "address": ":8080",
+            "tls": map[string]interface{}{
+                "enable": false,
+                "cert_file": "",
+                "key_file": ""
+            }
+        }
+    }
+}
+_ = config
+```
\ No newline at end of file
diff --git a/contrib/config/consul/config.go b/contrib/config/consul/config.go
index ce4317db..8025ec96 100644
--- a/contrib/config/consul/config.go
+++ b/contrib/config/consul/config.go
@@ -3,6 +3,7 @@ package consul
 import (
 	"context"
 	"errors"
+	"path/filepath"
 	"strings"
 
 	"github.com/go-kratos/kratos/v2/config"
@@ -67,12 +68,13 @@ func (s *source) Load() ([]*config.KeyValue, error) {
 	if !strings.HasSuffix(s.options.path, "/") {
 		pathPrefix = pathPrefix + "/"
 	}
-
 	kvs := make([]*config.KeyValue, 0)
 	for _, item := range kv {
+		k := strings.TrimPrefix(item.Key, pathPrefix)
 		kvs = append(kvs, &config.KeyValue{
-			Key:   item.Key[len(pathPrefix):],
-			Value: item.Value,
+			Key:    k,
+			Value:  item.Value,
+			Format: strings.TrimPrefix(filepath.Ext(k), "."),
 		})
 	}
 	return kvs, nil
diff --git a/contrib/config/consul/go.mod b/contrib/config/consul/go.mod
index d2f0f095..7bbed1a9 100644
--- a/contrib/config/consul/go.mod
+++ b/contrib/config/consul/go.mod
@@ -5,6 +5,7 @@ go 1.15
 require (
 	github.com/go-kratos/kratos/v2 v2.0.5
 	github.com/hashicorp/consul/api v1.10.0
+	github.com/stretchr/testify v1.7.0
 )
 
 replace github.com/go-kratos/kratos/v2 => ../../../
diff --git a/contrib/config/etcd/README.md b/contrib/config/etcd/README.md
new file mode 100644
index 00000000..c6f3f1bd
--- /dev/null
+++ b/contrib/config/etcd/README.md
@@ -0,0 +1,41 @@
+# Etcd Config
+
+```go
+import (
+	"log"
+
+	cfg "github.com/go-kratos/kratos/contrib/config/etcd/v2"
+	"github.com/go-kratos/kratos/v2/config"
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"google.golang.org/grpc"
+)
+
+// create a etcd client
+client, err := clientv3.New(clientv3.Config{
+    Endpoints:   []string{"127.0.0.1:2379"},
+    DialTimeout: time.Second,
+    DialOptions: []grpc.DialOption{grpc.WithBlock()},
+})
+if err != nil {
+    log.Fatal(err)
+}
+
+// configure the source, "path" is required
+source, err := cfg.New(client, cfg.WithPath("/app-config"), cfg.WithPrefix(true))
+if err != nil {
+    log.Fatalln(err)
+}
+
+// create a config instance with source
+c := config.New(config.WithSource(source))
+defer c.Close()
+
+// acquire config value
+foo, err := c.Value("/app-config").String()
+if err != nil {
+    log.Println(err)
+}
+println(foo)
+
+```
+
diff --git a/contrib/config/etcd/config.go b/contrib/config/etcd/config.go
index a18f3dc2..ea027c2a 100644
--- a/contrib/config/etcd/config.go
+++ b/contrib/config/etcd/config.go
@@ -3,6 +3,8 @@ package etcd
 import (
 	"context"
 	"errors"
+	"path/filepath"
+	"strings"
 
 	"github.com/go-kratos/kratos/v2/config"
 	clientv3 "go.etcd.io/etcd/client/v3"
@@ -75,12 +77,13 @@ func (s *source) Load() ([]*config.KeyValue, error) {
 	if err != nil {
 		return nil, err
 	}
-
 	kvs := make([]*config.KeyValue, 0)
 	for _, item := range rsp.Kvs {
+		k := string(item.Key)
 		kvs = append(kvs, &config.KeyValue{
-			Key:   string(item.Key),
-			Value: item.Value,
+			Key:    k,
+			Value:  item.Value,
+			Format: strings.TrimPrefix(filepath.Ext(k), "."),
 		})
 	}
 	return kvs, nil
diff --git a/contrib/config/etcd/go.mod b/contrib/config/etcd/go.mod
index a94c8a73..047595ed 100644
--- a/contrib/config/etcd/go.mod
+++ b/contrib/config/etcd/go.mod
@@ -3,7 +3,9 @@ module github.com/go-kratos/kratos/contrib/config/etcd/v2
 go 1.16
 
 require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-kratos/kratos/v2 v2.1.0
+	github.com/stretchr/testify v1.7.0
 	go.etcd.io/etcd/client/v3 v3.5.0
 	google.golang.org/grpc v1.40.0
 )
diff --git a/contrib/config/kubernetes/config.go b/contrib/config/kubernetes/config.go
index 2e66fbc7..c66b837f 100644
--- a/contrib/config/kubernetes/config.go
+++ b/contrib/config/kubernetes/config.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"path/filepath"
+	"strings"
 
 	"github.com/go-kratos/kratos/v2/config"
 	v1 "k8s.io/api/core/v1"
@@ -116,9 +118,12 @@ func (k *kube) load() (kvs []*config.KeyValue, err error) {
 
 func (k *kube) configMap(cm v1.ConfigMap) (kvs []*config.KeyValue) {
 	for name, val := range cm.Data {
+		k := fmt.Sprintf("%s/%s/%s", k.opts.Namespace, cm.Name, name)
+
 		kvs = append(kvs, &config.KeyValue{
-			Key:   fmt.Sprintf("%s/%s/%s", k.opts.Namespace, cm.Name, name),
-			Value: []byte(val),
+			Key:    k,
+			Value:  []byte(val),
+			Format: strings.TrimPrefix(filepath.Ext(k), "."),
 		})
 	}
 	return kvs
diff --git a/contrib/config/nacos/config.go b/contrib/config/nacos/config.go
index 3d5cbf4b..ee07e411 100644
--- a/contrib/config/nacos/config.go
+++ b/contrib/config/nacos/config.go
@@ -2,6 +2,8 @@ package config
 
 import (
 	"context"
+	"path/filepath"
+	"strings"
 	"time"
 
 	"github.com/go-kratos/kratos/v2/config"
@@ -89,11 +91,12 @@ func (c *Config) Load() ([]*config.KeyValue, error) {
 	if err != nil {
 		return nil, err
 	}
-
+	k := c.opts.dataID
 	return []*config.KeyValue{
 		{
-			Key:   c.opts.dataID,
-			Value: []byte(content),
+			Key:    k,
+			Value:  []byte(content),
+			Format: strings.TrimPrefix(filepath.Ext(k), "."),
 		},
 	}, nil
 }
@@ -144,10 +147,12 @@ func (w *Watcher) Next() ([]*config.KeyValue, error) {
 	case <-w.Context.Done():
 		return nil, nil
 	case content := <-w.content:
+		k := w.dataID
 		return []*config.KeyValue{
 			{
-				Key:   w.dataID,
-				Value: []byte(content),
+				Key:    k,
+				Value:  []byte(content),
+				Format: strings.TrimPrefix(filepath.Ext(k), "."),
 			},
 		}, nil
 	}
diff --git a/contrib/registry/consul/client.go b/contrib/registry/consul/client.go
index a20588ed..62ae663a 100644
--- a/contrib/registry/consul/client.go
+++ b/contrib/registry/consul/client.go
@@ -3,6 +3,7 @@ package consul
 import (
 	"context"
 	"fmt"
+	"net"
 	"net/url"
 	"strconv"
 	"strings"
@@ -69,15 +70,15 @@ func (d *Client) Service(ctx context.Context, service string, index uint64, pass
 // Register register service instacen to consul
 func (d *Client) Register(ctx context.Context, svc *registry.ServiceInstance, enableHealthCheck bool) error {
 	addresses := make(map[string]api.ServiceAddress)
-	var addr string
-	var port uint64
+	checkAddresses := make([]string, 0, len(svc.Endpoints))
 	for _, endpoint := range svc.Endpoints {
 		raw, err := url.Parse(endpoint)
 		if err != nil {
 			return err
 		}
-		addr = raw.Hostname()
-		port, _ = strconv.ParseUint(raw.Port(), 10, 16)
+		addr := raw.Hostname()
+		port, _ := strconv.ParseUint(raw.Port(), 10, 16)
+		checkAddresses = append(checkAddresses, fmt.Sprintf("%s:%d", addr, port))
 		addresses[raw.Scheme] = api.ServiceAddress{Address: endpoint, Port: int(port)}
 	}
 	asr := &api.AgentServiceRegistration{
@@ -86,24 +87,21 @@ func (d *Client) Register(ctx context.Context, svc *registry.ServiceInstance, en
 		Meta:            svc.Metadata,
 		Tags:            []string{fmt.Sprintf("version=%s", svc.Version)},
 		TaggedAddresses: addresses,
-		Address:         addr,
-		Port:            int(port),
-		Checks: []*api.AgentServiceCheck{
-			{
-				CheckID:                        "service:" + svc.ID,
-				TTL:                            "30s",
-				Status:                         "passing",
-				DeregisterCriticalServiceAfter: "90s",
-			},
-		},
+	}
+	if len(checkAddresses) > 0 {
+		host, portRaw, _ := net.SplitHostPort(checkAddresses[0])
+		port, _ := strconv.ParseInt(portRaw, 10, 32)
+		asr.Address = host
+		asr.Port = int(port)
 	}
 	if enableHealthCheck {
-		asr.Checks = append(asr.Checks, &api.AgentServiceCheck{
-			TCP:                            fmt.Sprintf("%s:%d", addr, port),
-			Interval:                       "20s",
-			Status:                         "passing",
-			DeregisterCriticalServiceAfter: "90s",
-		})
+		for _, address := range checkAddresses {
+			asr.Checks = append(asr.Checks, &api.AgentServiceCheck{
+				TCP:                            address,
+				Interval:                       "20s",
+				DeregisterCriticalServiceAfter: "70s",
+			})
+		}
 	}
 	err := d.cli.Agent().ServiceRegister(asr)
 	if err != nil {
diff --git a/contrib/registry/discovery/README.md b/contrib/registry/discovery/README.md
index 48bd654f..b220b629 100644
--- a/contrib/registry/discovery/README.md
+++ b/contrib/registry/discovery/README.md
@@ -1,4 +1,97 @@
-# Discovery Registry
+## Discovery Registry 
 
-## [discovery](https://github.com/bilibili/discovery)
+This module implements a `registry.Registrar` and `registry.Discovery` interface in kratos based `bilibili/discovery`.
+
+[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/go-kratos/kratos/contrib/registry/discovery/v2)
+
+### Quick Start
+
+**_Register a service_**
+
+```go
+import (
+	"github.com/go-kratos/kratos/contrib/registry/discovery/v2"
+)
+
+func main() {
+	logger := log.NewStdLogger(os.Stdout)
+	logger = log.With(logger, "service", "example.registry.discovery")
+	
+	// initialize a registry
+	r := discovery.New(&discovery.Config{
+		Nodes:  []string{"0.0.0.0:7171"},
+		Env:    "dev",
+		Region: "sh1",
+		Zone:   "zone1",
+		Host:   "hostname",
+	}, logger)
+
+	// construct srv instance
+	// ...
+	
+	app := kratos.New(
+		kratos.Name("helloworld"),
+		kratos.Server(
+			httpSrv,
+			grpcSrv,
+		),
+		kratos.Metadata(map[string]string{"color": "gray"}),
+		// use Registrar
+		kratos.Registrar(r),
+	)
+	
+	if err := app.Run(); err != nil {
+		log.NewHelper(logger).Fatal(err)
+	}	
+}
+```
+
+**_Discover a service_**
+
+```go
+import (
+	"github.com/go-kratos/kratos/contrib/registry/discovery/v2"
+	"github.com/go-kratos/kratos/v2/transport/grpc"
+)
+
+func main() {
+	// initialize a discovery
+	r := discovery.New(&discovery.Config{
+		Nodes:  []string{"0.0.0.0:7171"},
+		Env:    "dev",
+		Region: "sh1",
+		Zone:   "zone1",
+		Host:   "localhost",
+	}, nil)
+
+	conn, err := grpc.DialInsecure(
+		context.Background(),
+		grpc.WithEndpoint("discovery:///appid"),
+		// use discovery
+		grpc.WithDiscovery(r),
+	)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer conn.Close()
+	
+	// request and log
+}
+```
+
+### Config explain
+
+```go
+type Config struct {
+	Nodes  []string // discovery nodes address
+	Region string   // region of the service, sh
+	Zone   string   // zone of region, sh001
+	Env    string   // env of service, dev, prod and etc
+	Host   string   // hostname of service
+}
+```
+
+### References 
+
+- [bilibili/discovery](https://github.com/bilibili/discovery)
 
diff --git a/contrib/registry/kubernetes/registry.go b/contrib/registry/kubernetes/registry.go
index 16adf076..14b21f9a 100644
--- a/contrib/registry/kubernetes/registry.go
+++ b/contrib/registry/kubernetes/registry.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"net/url"
 	"os"
 	"strconv"
@@ -249,7 +248,7 @@ var currentNamespace = LoadNamespace()
 
 // LoadNamespace is used to get the current namespace from the file
 func LoadNamespace() string {
-	data, err := ioutil.ReadFile(ServiceAccountNamespacePath)
+	data, err := os.ReadFile(ServiceAccountNamespacePath)
 	if err != nil {
 		return ""
 	}
diff --git a/encoding/form/proto_decode.go b/encoding/form/proto_decode.go
index 9936b8e9..abf689a7 100644
--- a/encoding/form/proto_decode.go
+++ b/encoding/form/proto_decode.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 	"time"
 
+	"google.golang.org/protobuf/types/known/structpb"
+
 	"google.golang.org/genproto/protobuf/field_mask"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
@@ -33,17 +35,12 @@ func populateFieldValues(v protoreflect.Message, fieldPath []string, values []st
 	if len(values) < 1 {
 		return errors.New("no value provided")
 	}
+
 	var fd protoreflect.FieldDescriptor
 	for i, fieldName := range fieldPath {
-		fields := v.Descriptor().Fields()
-		if fd = getDescriptorByFieldAndName(fields, fieldName); fd == nil {
-			if len(fieldName) > 2 && strings.HasSuffix(fieldName, "[]") {
-				fd = getDescriptorByFieldAndName(fields, strings.TrimSuffix(fieldName, "[]"))
-			}
-			if fd == nil {
-				// ignore unexpected field.
-				return nil
-			}
+		if fd = getFieldDescriptor(v, fieldName); fd == nil {
+			// ignore unexpected field.
+			return nil
 		}
 
 		if i == len(fieldPath)-1 {
@@ -51,6 +48,10 @@ func populateFieldValues(v protoreflect.Message, fieldPath []string, values []st
 		}
 
 		if fd.Message() == nil || fd.Cardinality() == protoreflect.Repeated {
+			if fd.IsMap() && len(fieldPath) > 1 {
+				// post sub field
+				return populateMapField(fd, v.Mutable(fd).Map(), []string{fieldPath[1]}, values)
+			}
 			return fmt.Errorf("invalid path: %q is not a message", fieldName)
 		}
 
@@ -65,7 +66,7 @@ func populateFieldValues(v protoreflect.Message, fieldPath []string, values []st
 	case fd.IsList():
 		return populateRepeatedField(fd, v.Mutable(fd).List(), values)
 	case fd.IsMap():
-		return populateMapField(fd, v.Mutable(fd).Map(), values)
+		return populateMapField(fd, v.Mutable(fd).Map(), fieldPath, values)
 	}
 	if len(values) > 1 {
 		return fmt.Errorf("too many values for field %q: %s", fd.FullName().Name(), strings.Join(values, ", "))
@@ -73,13 +74,23 @@ func populateFieldValues(v protoreflect.Message, fieldPath []string, values []st
 	return populateField(fd, v, values[0])
 }
 
+func getFieldDescriptor(v protoreflect.Message, fieldName string) protoreflect.FieldDescriptor {
+	fields := v.Descriptor().Fields()
+	var fd protoreflect.FieldDescriptor
+	if fd = getDescriptorByFieldAndName(fields, fieldName); fd == nil {
+		if v.Descriptor().FullName() == structMessageFullname {
+			fd = fields.ByNumber(structFieldsFieldNumber)
+		} else if len(fieldName) > 2 && strings.HasSuffix(fieldName, "[]") {
+			fd = getDescriptorByFieldAndName(fields, strings.TrimSuffix(fieldName, "[]"))
+		}
+	}
+	return fd
+}
+
 func getDescriptorByFieldAndName(fields protoreflect.FieldDescriptors, fieldName string) protoreflect.FieldDescriptor {
 	var fd protoreflect.FieldDescriptor
 	if fd = fields.ByName(protoreflect.Name(fieldName)); fd == nil {
 		fd = fields.ByJSONName(fieldName)
-		if fd == nil {
-			return nil
-		}
 	}
 	return fd
 }
@@ -104,15 +115,17 @@ func populateRepeatedField(fd protoreflect.FieldDescriptor, list protoreflect.Li
 	return nil
 }
 
-func populateMapField(fd protoreflect.FieldDescriptor, mp protoreflect.Map, values []string) error {
-	if len(values) != 2 { //nolint:gomnd
-		return fmt.Errorf("more than one value provided for key %q in map %q", values[0], fd.FullName())
-	}
-	key, err := parseField(fd.MapKey(), values[0])
+func populateMapField(fd protoreflect.FieldDescriptor, mp protoreflect.Map, fieldPath []string, values []string) error {
+	flen := len(fieldPath)
+	vlen := len(values)
+	// post sub key.
+	nkey := flen - 1
+	key, err := parseField(fd.MapKey(), fieldPath[nkey])
 	if err != nil {
 		return fmt.Errorf("parsing map key %q: %w", fd.FullName().Name(), err)
 	}
-	value, err := parseField(fd.MapValue(), values[1])
+	vkey := vlen - 1
+	value, err := parseField(fd.MapValue(), values[vkey])
 	if err != nil {
 		return fmt.Errorf("parsing map value %q: %w", fd.FullName().Name(), err)
 	}
@@ -274,6 +287,12 @@ func parseMessage(md protoreflect.MessageDescriptor, value string) (protoreflect
 		fm := &field_mask.FieldMask{}
 		fm.Paths = append(fm.Paths, strings.Split(value, ",")...)
 		msg = fm
+	case "google.protobuf.Value":
+		fm, err := structpb.NewValue(value)
+		if err != nil {
+			return protoreflect.Value{}, err
+		}
+		msg = fm
 	default:
 		return protoreflect.Value{}, fmt.Errorf("unsupported message type: %q", string(md.FullName()))
 	}
diff --git a/encoding/form/well_known_types.go b/encoding/form/well_known_types.go
index 77959492..f87ca640 100644
--- a/encoding/form/well_known_types.go
+++ b/encoding/form/well_known_types.go
@@ -27,6 +27,10 @@ const (
 	// bytes
 	bytesMessageFullname  protoreflect.FullName    = "google.protobuf.BytesValue"
 	bytesValueFieldNumber protoreflect.FieldNumber = 1
+
+	// google.protobuf.Struct.
+	structMessageFullname   protoreflect.FullName    = "google.protobuf.Struct"
+	structFieldsFieldNumber protoreflect.FieldNumber = 1
 )
 
 func marshalTimestamp(m protoreflect.Message) (string, error) {
diff --git a/examples/config/t/a.yaml b/examples/config/t/a.yaml
new file mode 100644
index 00000000..91a2ebf7
--- /dev/null
+++ b/examples/config/t/a.yaml
@@ -0,0 +1,5 @@
+foo:
+  baz: "2"
+  biu: "example"
+hello:
+  a: b
\ No newline at end of file
diff --git a/examples/config/t/b.yaml b/examples/config/t/b.yaml
new file mode 100644
index 00000000..d51ff07a
--- /dev/null
+++ b/examples/config/t/b.yaml
@@ -0,0 +1,6 @@
+foo:
+  bar: 3
+  baz: aaaa
+hey:
+  good: bad
+  qux: quux
\ No newline at end of file
diff --git a/examples/tls/client/main.go b/examples/tls/client/main.go
index 1c9d52a2..54331af1 100644
--- a/examples/tls/client/main.go
+++ b/examples/tls/client/main.go
@@ -4,8 +4,8 @@ import (
 	"context"
 	"crypto/tls"
 	"crypto/x509"
-	"io/ioutil"
 	"log"
+	"os"
 
 	pb "github.com/go-kratos/kratos/examples/helloworld/helloworld"
 	"github.com/go-kratos/kratos/v2/transport/grpc"
@@ -14,7 +14,7 @@ import (
 
 func main() {
 	// Load CA certificate pem file.
-	b, err := ioutil.ReadFile("../cert/ca.crt")
+	b, err := os.ReadFile("../cert/ca.crt")
 	if err != nil {
 		panic(err)
 	}
diff --git a/middleware/tracing/tracer.go b/middleware/tracing/tracer.go
index bb606ff1..48d4eaa1 100644
--- a/middleware/tracing/tracer.go
+++ b/middleware/tracing/tracer.go
@@ -34,9 +34,9 @@ func NewTracer(kind trace.SpanKind, opts ...Option) *Tracer {
 
 	switch kind {
 	case trace.SpanKindClient:
-		return &Tracer{tracer: otel.Tracer("kartos"), kind: kind, opt: &op}
+		return &Tracer{tracer: otel.Tracer("kratos"), kind: kind, opt: &op}
 	case trace.SpanKindServer:
-		return &Tracer{tracer: otel.Tracer("kartos"), kind: kind, opt: &op}
+		return &Tracer{tracer: otel.Tracer("kratos"), kind: kind, opt: &op}
 	default:
 		panic(fmt.Sprintf("unsupported span kind: %v", kind))
 	}
diff --git a/transport/http/client.go b/transport/http/client.go
index b792bccc..c40c43bb 100644
--- a/transport/http/client.go
+++ b/transport/http/client.go
@@ -6,7 +6,6 @@ import (
 	"crypto/tls"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"time"
 
@@ -322,7 +321,7 @@ func DefaultRequestEncoder(ctx context.Context, contentType string, in interface
 // DefaultResponseDecoder is an HTTP response decoder.
 func DefaultResponseDecoder(ctx context.Context, res *http.Response, v interface{}) error {
 	defer res.Body.Close()
-	data, err := ioutil.ReadAll(res.Body)
+	data, err := io.ReadAll(res.Body)
 	if err != nil {
 		return err
 	}
@@ -335,7 +334,7 @@ func DefaultErrorDecoder(ctx context.Context, res *http.Response) error {
 		return nil
 	}
 	defer res.Body.Close()
-	data, err := ioutil.ReadAll(res.Body)
+	data, err := io.ReadAll(res.Body)
 	if err == nil {
 		e := new(errors.Error)
 		if err = CodecForResponse(res).Unmarshal(data, e); err == nil {
diff --git a/transport/http/codec.go b/transport/http/codec.go
index 7eb62872..ef00ea6e 100644
--- a/transport/http/codec.go
+++ b/transport/http/codec.go
@@ -1,7 +1,7 @@
 package http
 
 import (
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/go-kratos/kratos/v2/encoding"
@@ -27,7 +27,7 @@ func DefaultRequestDecoder(r *http.Request, v interface{}) error {
 	if !ok {
 		return errors.BadRequest("CODEC", r.Header.Get("Content-Type"))
 	}
-	data, err := ioutil.ReadAll(r.Body)
+	data, err := io.ReadAll(r.Body)
 	if err != nil {
 		return errors.BadRequest("CODEC", err.Error())
 	}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 5f1f9474..27f453c1 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -138,6 +138,7 @@ func TestConfig(t *testing.T) {
 	cf := &config{}
 	cf.opts = opts
 	cf.reader = newReader(opts)
+	cf.log = log.NewHelper(opts.logger)
 
 	err = cf.Load()
 	assert.Nil(t, err)
diff --git a/config/env/env_test.go b/config/env/env_test.go
index a63e88ee..bbc8a8d7 100644
--- a/config/env/env_test.go
+++ b/config/env/env_test.go
@@ -1,7 +1,6 @@
 package env
 
 import (
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -39,7 +38,7 @@ func TestEnvWithPrefix(t *testing.T) {
 	if err := os.MkdirAll(path, 0o700); err != nil {
 		t.Error(err)
 	}
-	if err := ioutil.WriteFile(filename, data, 0o666); err != nil {
+	if err := os.WriteFile(filename, data, 0o666); err != nil {
 		t.Error(err)
 	}
 
@@ -149,7 +148,7 @@ func TestEnvWithoutPrefix(t *testing.T) {
 	if err := os.MkdirAll(path, 0o700); err != nil {
 		t.Error(err)
 	}
-	if err := ioutil.WriteFile(filename, data, 0o666); err != nil {
+	if err := os.WriteFile(filename, data, 0o666); err != nil {
 		t.Error(err)
 	}
 
diff --git a/config/file/file_test.go b/config/file/file_test.go
index d485e1f8..a9b5d24e 100644
--- a/config/file/file_test.go
+++ b/config/file/file_test.go
@@ -2,7 +2,6 @@ package file
 
 import (
 	"errors"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"sync"
@@ -93,7 +92,7 @@ func TestFile(t *testing.T) {
 	if err := os.MkdirAll(path, 0o700); err != nil {
 		t.Error(err)
 	}
-	if err := ioutil.WriteFile(file, data, 0o666); err != nil {
+	if err := os.WriteFile(file, data, 0o666); err != nil {
 		t.Error(err)
 	}
 	testSource(t, file, data)
@@ -181,7 +180,7 @@ func testSource(t *testing.T, path string, data []byte) {
 func TestConfig(t *testing.T) {
 	path := filepath.Join(t.TempDir(), "test_config.json")
 	defer os.Remove(path)
-	if err := ioutil.WriteFile(path, []byte(_testJSON), 0o666); err != nil {
+	if err := os.WriteFile(path, []byte(_testJSON), 0o666); err != nil {
 		t.Error(err)
 	}
 	c := config.New(config.WithSource(
@@ -293,7 +292,7 @@ func testScan(t *testing.T, c config.Config) {
 func TestMergeDataRace(t *testing.T) {
 	path := filepath.Join(t.TempDir(), "test_config.json")
 	defer os.Remove(path)
-	if err := ioutil.WriteFile(path, []byte(_testJSON), 0o666); err != nil {
+	if err := os.WriteFile(path, []byte(_testJSON), 0o666); err != nil {
 		t.Error(err)
 	}
 	c := config.New(config.WithSource(
diff --git a/contrib/config/consul/config_test.go b/contrib/config/consul/config_test.go
index b47848fe..4f07aaa5 100644
--- a/contrib/config/consul/config_test.go
+++ b/contrib/config/consul/config_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/hashicorp/consul/api"
+	"github.com/stretchr/testify/assert"
 )
 
 const testPath = "kratos/test/config"
@@ -60,3 +61,34 @@ func TestConfig(t *testing.T) {
 		t.Error(err)
 	}
 }
+
+func TestExtToFormat(t *testing.T) {
+	client, err := api.NewClient(&api.Config{
+		Address: "127.0.0.1:8500",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	tp := "kratos/test/ext"
+	tn := "a.bird.json"
+	tk := tp + "/" + tn
+	tc := `{"a":1}`
+	if _, err = client.KV().Put(&api.KVPair{Key: tk, Value: []byte(tc)}, nil); err != nil {
+		t.Fatal(err)
+	}
+
+	source, err := New(client, WithPath(tp))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	kvs, err := source.Load()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, 1, len(kvs))
+	assert.Equal(t, tn, kvs[0].Key)
+	assert.Equal(t, tc, string(kvs[0].Value))
+	assert.Equal(t, "json", kvs[0].Format)
+}
diff --git a/contrib/config/etcd/config_test.go b/contrib/config/etcd/config_test.go
index c83eb690..f0205738 100644
--- a/contrib/config/etcd/config_test.go
+++ b/contrib/config/etcd/config_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/assert"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"google.golang.org/grpc"
 )
@@ -64,3 +65,39 @@ func TestConfig(t *testing.T) {
 		t.Error(err)
 	}
 }
+
+func TestExtToFormat(t *testing.T) {
+	client, err := clientv3.New(clientv3.Config{
+		Endpoints:   []string{"127.0.0.1:2379"},
+		DialTimeout: time.Second, DialOptions: []grpc.DialOption{grpc.WithBlock()},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		_ = client.Close()
+	}()
+
+	tp := "/kratos/test/ext"
+	tn := "a.bird.json"
+	tk := tp + "/" + tn
+	tc := `{"a":1}`
+	if _, err = client.Put(context.Background(), tk, tc); err != nil {
+		t.Fatal(err)
+	}
+
+	source, err := New(client, WithPath(tp), WithPrefix(true))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	kvs, err := source.Load()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, 1, len(kvs))
+	assert.Equal(t, tk, kvs[0].Key)
+	assert.Equal(t, tc, string(kvs[0].Value))
+	assert.Equal(t, "json", kvs[0].Format)
+}
diff --git a/contrib/log/fluent/fluent_test.go b/contrib/log/fluent/fluent_test.go
index e42ce1c0..8558f31c 100644
--- a/contrib/log/fluent/fluent_test.go
+++ b/contrib/log/fluent/fluent_test.go
@@ -1,7 +1,7 @@
 package fluent
 
 import (
-	"io/ioutil"
+	"io"
 	"net"
 	"os"
 	"testing"
@@ -19,7 +19,7 @@ func TestMain(m *testing.M) {
 					return
 				}
 				defer conn.Close()
-				if _, err = ioutil.ReadAll(conn); err != nil {
+				if _, err = io.ReadAll(conn); err != nil {
 					continue
 				}
 			}
diff --git a/examples/tls/tls_test.go b/examples/tls/tls_test.go
index 27c961bf..70745823 100644
--- a/examples/tls/tls_test.go
+++ b/examples/tls/tls_test.go
@@ -5,7 +5,7 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"fmt"
-	"io/ioutil"
+	"os"
 	"testing"
 	"time"
 
@@ -100,7 +100,7 @@ func TestETCD(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	b, err := ioutil.ReadFile("./cert/server.crt")
+	b, err := os.ReadFile("./cert/server.crt")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/log/filter_test.go b/log/filter_test.go
index 4033a7c2..4a8b249f 100644
--- a/log/filter_test.go
+++ b/log/filter_test.go
@@ -1,7 +1,7 @@
 package log
 
 import (
-	"io/ioutil"
+	"io"
 	"testing"
 )
 
@@ -58,21 +58,21 @@ func TestFilterFunc(t *testing.T) {
 }
 
 func BenchmarkFilterKey(b *testing.B) {
-	log := NewHelper(NewFilter(NewStdLogger(ioutil.Discard), FilterKey("password")))
+	log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterKey("password")))
 	for i := 0; i < b.N; i++ {
 		log.Infow("password", "123456")
 	}
 }
 
 func BenchmarkFilterValue(b *testing.B) {
-	log := NewHelper(NewFilter(NewStdLogger(ioutil.Discard), FilterValue("password")))
+	log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterValue("password")))
 	for i := 0; i < b.N; i++ {
 		log.Infow("password")
 	}
 }
 
 func BenchmarkFilterFunc(b *testing.B) {
-	log := NewHelper(NewFilter(NewStdLogger(ioutil.Discard), FilterFunc(testFilterFunc)))
+	log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterFunc(testFilterFunc)))
 	for i := 0; i < b.N; i++ {
 		log.Info("password", "123456")
 	}
diff --git a/log/helper_test.go b/log/helper_test.go
index a9c1a68b..dc888a39 100644
--- a/log/helper_test.go
+++ b/log/helper_test.go
@@ -2,7 +2,7 @@ package log
 
 import (
 	"context"
-	"io/ioutil"
+	"io"
 	"os"
 	"testing"
 )
@@ -40,21 +40,21 @@ func TestHelperLevel(t *testing.T) {
 }
 
 func BenchmarkHelperPrint(b *testing.B) {
-	log := NewHelper(NewStdLogger(ioutil.Discard))
+	log := NewHelper(NewStdLogger(io.Discard))
 	for i := 0; i < b.N; i++ {
 		log.Debug("test")
 	}
 }
 
 func BenchmarkHelperPrintf(b *testing.B) {
-	log := NewHelper(NewStdLogger(ioutil.Discard))
+	log := NewHelper(NewStdLogger(io.Discard))
 	for i := 0; i < b.N; i++ {
 		log.Debugf("%s", "test")
 	}
 }
 
 func BenchmarkHelperPrintw(b *testing.B) {
-	log := NewHelper(NewStdLogger(ioutil.Discard))
+	log := NewHelper(NewStdLogger(io.Discard))
 	for i := 0; i < b.N; i++ {
 		log.Debugw("key", "value")
 	}
diff --git a/transport/http/client_test.go b/transport/http/client_test.go
index 3d9445e0..dd5af239 100644
--- a/transport/http/client_test.go
+++ b/transport/http/client_test.go
@@ -5,7 +5,7 @@ import (
 	"context"
 	"crypto/tls"
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	nethttp "net/http"
 	"testing"
 	"time"
@@ -135,7 +135,7 @@ func TestWithDiscovery(t *testing.T) {
 func TestDefaultRequestEncoder(t *testing.T) {
 	req1 := &nethttp.Request{
 		Header: make(nethttp.Header),
-		Body:   ioutil.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
+		Body:   io.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
 	}
 	req1.Header.Set("Content-Type", "application/xml")
 
@@ -158,7 +158,7 @@ func TestDefaultResponseDecoder(t *testing.T) {
 	resp1 := &nethttp.Response{
 		Header:     make(nethttp.Header),
 		StatusCode: 200,
-		Body:       ioutil.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
+		Body:       io.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
 	}
 	v1 := &struct {
 		A string `json:"a"`
@@ -172,7 +172,7 @@ func TestDefaultResponseDecoder(t *testing.T) {
 	resp2 := &nethttp.Response{
 		Header:     make(nethttp.Header),
 		StatusCode: 200,
-		Body:       ioutil.NopCloser(bytes.NewBufferString("{badjson}")),
+		Body:       io.NopCloser(bytes.NewBufferString("{badjson}")),
 	}
 	v2 := &struct {
 		A string `json:"a"`
@@ -191,14 +191,14 @@ func TestDefaultErrorDecoder(t *testing.T) {
 	resp1 := &nethttp.Response{
 		Header:     make(nethttp.Header),
 		StatusCode: 300,
-		Body:       ioutil.NopCloser(bytes.NewBufferString("{\"foo\":\"bar\"}")),
+		Body:       io.NopCloser(bytes.NewBufferString("{\"foo\":\"bar\"}")),
 	}
 	assert.Error(t, DefaultErrorDecoder(context.TODO(), resp1))
 
 	resp2 := &nethttp.Response{
 		Header:     make(nethttp.Header),
 		StatusCode: 500,
-		Body:       ioutil.NopCloser(bytes.NewBufferString("{\"code\":54321, \"message\": \"hi\", \"reason\": \"FOO\"}")),
+		Body:       io.NopCloser(bytes.NewBufferString("{\"code\":54321, \"message\": \"hi\", \"reason\": \"FOO\"}")),
 	}
 	err2 := DefaultErrorDecoder(context.TODO(), resp2)
 	assert.Error(t, err2)
diff --git a/transport/http/codec_test.go b/transport/http/codec_test.go
index 961e1269..7e8babd0 100644
--- a/transport/http/codec_test.go
+++ b/transport/http/codec_test.go
@@ -2,7 +2,7 @@ package http
 
 import (
 	"bytes"
-	"io/ioutil"
+	"io"
 	nethttp "net/http"
 	"testing"
 
@@ -13,7 +13,7 @@ import (
 func TestDefaultRequestDecoder(t *testing.T) {
 	req1 := &nethttp.Request{
 		Header: make(nethttp.Header),
-		Body:   ioutil.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
+		Body:   io.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
 	}
 	req1.Header.Set("Content-Type", "application/json")
 
@@ -83,7 +83,7 @@ func TestDefaultResponseEncoderWithError(t *testing.T) {
 func TestCodecForRequest(t *testing.T) {
 	req1 := &nethttp.Request{
 		Header: make(nethttp.Header),
-		Body:   ioutil.NopCloser(bytes.NewBufferString("<xml></xml>")),
+		Body:   io.NopCloser(bytes.NewBufferString("<xml></xml>")),
 	}
 	req1.Header.Set("Content-Type", "application/xml")
 
@@ -93,7 +93,7 @@ func TestCodecForRequest(t *testing.T) {
 
 	req2 := &nethttp.Request{
 		Header: make(nethttp.Header),
-		Body:   ioutil.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
+		Body:   io.NopCloser(bytes.NewBufferString("{\"a\":\"1\", \"b\": 2}")),
 	}
 	req2.Header.Set("Content-Type", "blablablabla")
 
diff --git a/transport/http/server_test.go b/transport/http/server_test.go
index 5be44e8a..b5238bb0 100644
--- a/transport/http/server_test.go
+++ b/transport/http/server_test.go
@@ -5,7 +5,7 @@ import (
 	"crypto/tls"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"strings"
 	"testing"
@@ -113,7 +113,7 @@ func testClient(t *testing.T, srv *Server) {
 			_ = resp.Body.Close()
 			t.Fatalf("http status got %d", resp.StatusCode)
 		}
-		content, err := ioutil.ReadAll(resp.Body)
+		content, err := io.ReadAll(resp.Body)
 		_ = resp.Body.Close()
 		if err != nil {
 			t.Fatalf("read resp error %v", err)