diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e8d5e0a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,47 @@
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+*.cache
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+*build
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+go.work.sum
+
+# env file
+.env
+
+# Other
+.cache/
+unpackage
+.idea/
+.DS_Store
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.node
+!.vscode/extensions.json
+
+logs
+*.log
+
+# resources
+dev/store
\ No newline at end of file
diff --git a/README.md b/README.md
index 51e3cf7..3666cde 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,444 @@
-# service-discovery
-This is a service discovery
+
+
Go Service Link
+
Service Discovery and Dynamic Configuration Management for Golang
+
+

+

+

+

+

+

+
+
+
+
+`Go Service Link` s a manager for service discovery and dynamic configuration, providing a collection of adaptable middleware for microservice architectures. It supports service registration and discovery, load balancing, dynamic configuration synchronization, real-time monitoring, and hot reloading, along with powerful features like connection pooling, health checks, and exponential backoff retries.
+
+> English | [中文](README_zh.md)
+
+⭐️ If this project helps you, please give it a star!
+
+## Features
+- **Service Registration**: Automatically registers service information with middleware upon startup, supporting lease mechanisms to ensure service state updates.
+- **Service Discovery**: Clients dynamically retrieve service lists from middleware, with real-time monitoring of service changes.
+- **Load Balancing**: Offers multiple load balancing strategies (Random, Round-Robin, Consistent-Hash).
+- **Supported Middleware**: Etcd, Consul, ZooKeeper, Nacos.
+- **Dynamic Configuration Management**: Synchronizes local and remote configurations, monitors configuration changes in real-time, and triggers hot reloading.
+- **Connection Pool**: Maintains client connection pools for each configuration center to improve performance and resource utilization.
+- **Distributed Locks**: Uses distributed locks (e.g., Etcd mutex, Consul lock, ZooKeeper lock) to ensure the safety of configuration updates.
+- **Health Checks**: Periodically checks the health status of configuration centers, providing metrics like latency, leader status, and cluster size.
+- **Reconnection and Retry**: Automatically reconnects to disconnected configuration centers and retries failed operations using an exponential backoff strategy.
+- **Graceful Shutdown**: Properly cleans up resources (connections, listeners) when the application terminates.
+- **Example Code**: Includes server/client examples demonstrating practical applications of service discovery.
+- **Modular Design**: Clear code structure (servicediscovery, dynaconfig), easy to extend and integrate.
+
+
+## Server Side
+
+Below is an example of server-side service registration code:
+
+```go
+var discovery servicediscovery.ServiceDiscovery
+
+// setupDiscovery .
+func setupDiscovery(serviceName, httPort, grpcPort string) error {
+ var err error
+ discovery, err = servicediscovery.NewServiceDiscovery(servicediscovery.Config{
+ //Type: servicediscovery.ServiceDiscoveryTypeEtcd,
+ //Addrs: "localhost:2379",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeConsul,
+ //Addrs: "localhost:8500",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeZookeeper,
+ //Addrs: "localhost:2181",
+
+ Type: servicediscovery.ServiceDiscoveryTypeNacos,
+ Addrs: "localhost:8848",
+ Username: "nacos",
+ Password: "nacos",
+
+ ServiceName: serviceName,
+ })
+ if err != nil {
+ return err
+ }
+
+ discovery.SetOutputLogCallback(func(logType servicediscovery.OutputLogType, message string) {
+ if logType == servicediscovery.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ return nil
+}
+
+func main() {
+ httpPort := flag.String("http-port", "8001", "Port for HTTP server")
+ grpcPort := flag.String("grpc-port", "9001", "Port for gRPC server")
+ flag.Parse()
+
+ serviceName := "hello-app"
+ host := "localhost"
+
+ err := setupDiscovery(serviceName, *httpPort, *grpcPort)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to initialize service discovery: %v\n", err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Register service
+ instanceID := uuid.New().String()
+ if err = discovery.Register(ctx, serviceName, instanceID, host, *httpPort, *grpcPort); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to register service: %v\n", err)
+ }
+
+ // Watch service
+ go watchInstances(ctx, discovery, serviceName, instanceID)
+
+ // Close
+ defer func() {
+ if err = discovery.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Service discovery close error: %v\n", err)
+ } else {
+ fmt.Fprintf(os.Stdout, "Service discovery closed successfully\n")
+ }
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+}
+
+// watchInstances ...
+func watchInstances(ctx context.Context, discovery servicediscovery.ServiceDiscovery, serviceName, instanceID string) {
+ if discovery == nil {
+ return
+ }
+
+ ch, err := discovery.Watch(ctx, serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to discovery watch: %v\n", err)
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ if discovery != nil {
+ if err = discovery.Deregister(ctx, serviceName, instanceID); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to deregister service: %v\n", err)
+ }
+ }
+ return
+ case instances, ok := <-ch:
+ if !ok {
+ return
+ }
+ instancesStr, _ := json.Marshal(instances)
+ fmt.Fprintf(os.Stdout, "Discovered instances: %d, list: %v \n", len(instances), string(instancesStr))
+ }
+ }
+}
+```
+
+## Client Side
+
+Below is an example of client-side service discovery and load balancing code:
+
+```go
+var discovery *servicediscovery.DiscoveryWithLB
+
+// setupDiscovery .
+func setupDiscovery(serviceName string) error {
+ var err error
+ discovery, err = servicediscovery.NewDiscoveryWithLB(servicediscovery.Config{
+ //Type: servicediscovery.ServiceDiscoveryTypeEtcd,
+ //Addrs: "localhost:2379",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeConsul,
+ //Addrs: "localhost:8500",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeZookeeper,
+ //Addrs: "localhost:2181",
+
+ Type: servicediscovery.ServiceDiscoveryTypeNacos,
+ Addrs: "localhost:8848",
+ Username: "nacos",
+ Password: "nacos",
+
+ ServiceName: serviceName,
+ }, balancer.LoadBalancerTypeRoundRobin)
+ if err != nil {
+ return err
+ }
+
+ discovery.SetOutputLogCallback(func(logType servicediscovery.OutputLogType, message string) {
+ if logType == servicediscovery.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ return nil
+}
+
+// watchInstances ...
+func watchInstances(ctx context.Context, discovery servicediscovery.ServiceDiscovery, serviceName string) {
+ if discovery == nil {
+ return
+ }
+
+ ch, err := discovery.Watch(ctx, serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to discovery watch: %v\n", err)
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case instances, ok := <-ch:
+ if !ok {
+ return
+ }
+ instancesStr, _ := json.Marshal(instances)
+ fmt.Fprintf(os.Stdout, "Discovered instances: %d, list: %v \n", len(instances), string(instancesStr))
+ }
+ }
+}
+
+func selectUrl(serviceName string) string {
+ hostname, _ := os.Hostname()
+ inst, err := discovery.Select(serviceName, hostname)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to select instance: %v\n", err)
+ return ""
+ }
+ httpPort := inst.HTTPPort
+ return fmt.Sprintf("http://%s:%s/hello", inst.Host, httpPort)
+}
+
+func callRequests(serviceName string, numWorkers, requestsPerWorker int) {
+ wg := sync.WaitGroup{}
+ for i := 0; i < numWorkers; i++ {
+ wg.Add(1)
+ go func(workerID int) {
+ defer wg.Done()
+ for j := 0; j < requestsPerWorker; j++ {
+ url := selectUrl(serviceName)
+ fmt.Fprintf(os.Stdout, "worker: %d, request: %d selectUrl: %v\n", workerID, j, url)
+ time.Sleep(10 * time.Millisecond)
+ }
+ }(i)
+ }
+
+ wg.Wait()
+}
+
+func main() {
+ numWorkers := flag.Int("worker", 5, "Port for HTTP server")
+ requestsPerWorker := flag.Int("request", 10, "Port for gRPC server")
+ flag.Parse()
+
+ serviceName := "hello-app"
+ err := setupDiscovery(serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to initialize service discovery: %v\n", err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ go watchInstances(ctx, discovery, serviceName)
+
+ // Close
+ defer func() {
+ if err = discovery.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Service discovery close error: %v\n", err)
+ } else {
+ fmt.Fprintf(os.Stdout, "Service discovery closed successfully\n")
+ }
+ }()
+
+ fmt.Println(">>>>>>> string call request ...")
+ go func() {
+ for {
+ callRequests(serviceName, *numWorkers, *requestsPerWorker)
+ time.Sleep(1 * time.Second)
+ }
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+}
+```
+
+
+
+
+
+## Dynamic Configuration Management
+> Synchronization Mechanism: Automatically updates based on version control. When the configuration center’s version is higher than the local version, it syncs to the local system; otherwise, it syncs to the configuration center.
+
+Below is an example of dynamic configuration usage:
+
+```go
+func main() {
+ configs := map[string]*provider.Config{
+ "/config/my-app/main": {
+ Name: "my-service-app-main",
+ //Version: 0,
+ Version: 2676136267083311000,
+ Content: `{"AppName": "my-app-main", "Port": 8081, DebugMode: false }`,
+ ValidateCallback: func(config *provider.Config) (skip bool, err error) {
+ if config.Content == "" {
+ return false, fmt.Errorf("contnet must be not empty")
+ }
+ return true, nil
+ },
+ },
+ "/config/my-app/db": {
+ Name: "my-service-app-db",
+ //Version: 0,
+ Version: 2676136267083311000,
+ Content: `{"AppName": "my-app-db", "Port": 3306 }`,
+ ValidateCallback: func(config *provider.Config) (skip bool, err error) {
+ if config.Content == "" {
+ return false, fmt.Errorf("contnet must be not empty")
+ }
+ return true, nil
+ },
+ },
+ }
+
+ providerCfg := provider.ProviderConfig{
+ //Type: provider.ProviderTypeEtcd,
+ //Endpoints: []string{"localhost:2379"},
+
+ //Type: provider.ProviderTypeConsul,
+ //Endpoints: []string{"localhost:8500"},
+
+ //Type: provider.ProviderTypeZookeeper,
+ //Endpoints: []string{"localhost:2181"},
+
+ Type: provider.ProviderTypeNacos,
+ Endpoints: []string{"localhost:8848"},
+ Username: "nacos",
+ Password: "nacos",
+ //NacosProviderConfig: provider.NacosProviderConfig{
+ // NacosExtraConfig: extraconfig.NacosExtraConfig{
+ // NamespaceId: "",
+ // },
+ //},
+ }
+
+ manager, err := dynaconfig.NewConfigManager(dynaconfig.ConfigManagerParams{
+ ProviderConfig: providerCfg,
+ Configs: configs,
+ })
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to create config mananger, err: %v \n", err)
+ }
+ manager.SetOutputLogCallback(func(logType dynaconfig.OutputLogType, message string) {
+ if logType == dynaconfig.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == dynaconfig.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == dynaconfig.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ log.Println(">>>>>>>>>>>> Hot reload triggered", "key", key, "content", config.Content)
+ if key == "/config/my-app/db" {
+ if helper.IsOnlyEmpty(config.Content) {
+ return errors.New("invalid port number")
+ }
+ fmt.Fprintf(os.Stderr, ">>>>>>>>>>>>>>> Reinitializing database connection, content: %v \n", config.Content)
+ }
+ return nil
+ })
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ if key == "/config/my-app/main" {
+ // test panic
+ //panic("Simulated panic in callback")
+ }
+ return nil
+ })
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ manager.ASyncConfig(ctx)
+ //if err = manager.SyncConfig(context.Background()); err != nil {
+ // fmt.Fprintf(os.Stderr, "Failed to sync config: %v\n", err)
+ // return
+ //}
+
+ if err = manager.Watch(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to start watch: %v \n", err)
+ return
+ }
+
+ defer func() {
+ if err = manager.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to close: %v \n", err)
+ }
+ }()
+
+ //////////////////////// testing /////////////////////////
+ // Testing read the configuration content in real time
+ go func() {
+ for {
+ time.Sleep(3 * time.Second)
+ for _, key := range []string{"/config/my-app/main", "/config/my-app/db"} {
+ config := manager.GetLocalConfig(key)
+ fmt.Printf("+++++++ >>> Current config for %s: %+v\n", key, config)
+ }
+ }
+ }()
+ /////////////////////////////////////////////////
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+
+}
+```
+
+## LICENSE
+
+MIT
\ No newline at end of file
diff --git a/README_zh.md b/README_zh.md
new file mode 100644
index 0000000..07f5489
--- /dev/null
+++ b/README_zh.md
@@ -0,0 +1,477 @@
+
+
Go Service Link
+
用于 Golang 的服务发现和服务动态配置管理
+
+

+

+

+

+

+

+
+
+
+
+`Go Service Link` 是服务发现和动态配置的管理器,提供多种中间件可适配的合集,适用于微服务架构,支持服务注册与发现、负载均衡、动态配置同步、实时监控和热加载等功能,同时具备连接池、健康检查和指数退避重试等强大功能。
+
+
+
+> [English](README.md) | 中文
+ ⭐️ 如果能帮助到你,请随手给点一个star
+
+## 功能特性
+- **服务注册**:服务启动时自动向服务中间件注册服务信息,支持租约机制确保服务状态更新。
+- **服务发现**:客户端从服务中间件动态获取服务列表,支持实时监听服务变化。
+- **负载均衡**:提供轮询多种负载均衡策略(Random、Round-Robin、Consistent-Hash)。
+- **支持中间件**:Etcd、Consul、ZooKeeper、Nacos。
+- **动态配置管理**: 同步本地和远程配置、实时监控配置变化并触发热加载。
+- **连接池**:为每个配置中心维护客户端连接池,提升性能和资源利用率。
+- **分布式锁**:使用分布式锁(例如 Etcd mutex、Consul 锁、ZooKeeper 锁)确保配置更新的安全性。
+- **健康检查**:定期检查配置中心的健康状态,提供延迟、领导者状态和集群规模等指标。
+- **重连和重试**: 自动重连断开的配置中心、使用指数退避策略重试失败的操作。
+- **优雅关闭**:在应用终止时正确清理资源(连接、监听器)。
+- **示例代码**:包含服务端/客户端示例,展示服务发现的实际应用。
+- **模块化设计**:代码结构清晰(servicediscovery、dynaconfig),易于扩展和集成。
+
+
+
+
+### 设置Go代理
+- Window
+```shell
+$ set GO111MODULE=on
+$ set GOPROXY=https://goproxy.io,direct
+
+### The Golang 1.13+ can be executed directly
+$ go env -w GO111MODULE=on
+$ go env -w GOPROXY=https://goproxy.io,direct
+```
+- Linux or Mac
+```shell
+$ export GO111MODULE=on
+$ export GOPROXY=https://goproxy.io,direct
+
+### or
+$ echo "export GO111MODULE=on" >> ~/.profile
+$ echo "export GOPROXY=https://goproxy.cn,direct" >> ~/.profile
+$ source ~/.profile
+```
+
+### 安装
+```shell
+$ go get -u github.com/wenlng/go-service-link@latest
+```
+
+---
+
+## 服务发现 - Service 端
+下面是一个服务器端服务注册代码的例子:
+
+```go
+var discovery servicediscovery.ServiceDiscovery
+
+// setupDiscovery .
+func setupDiscovery(serviceName, httPort, grpcPort string) error {
+ var err error
+ discovery, err = servicediscovery.NewServiceDiscovery(servicediscovery.Config{
+ //Type: servicediscovery.ServiceDiscoveryTypeEtcd,
+ //Addrs: "localhost:2379",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeConsul,
+ //Addrs: "localhost:8500",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeZookeeper,
+ //Addrs: "localhost:2181",
+
+ Type: servicediscovery.ServiceDiscoveryTypeNacos,
+ Addrs: "localhost:8848",
+ Username: "nacos",
+ Password: "nacos",
+
+ ServiceName: serviceName,
+ })
+ if err != nil {
+ return err
+ }
+
+ discovery.SetOutputLogCallback(func(logType servicediscovery.OutputLogType, message string) {
+ if logType == servicediscovery.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ return nil
+}
+
+func main() {
+ httpPort := flag.String("http-port", "8001", "Port for HTTP server")
+ grpcPort := flag.String("grpc-port", "9001", "Port for gRPC server")
+ flag.Parse()
+
+ serviceName := "hello-app"
+ host := "localhost"
+
+ err := setupDiscovery(serviceName, *httpPort, *grpcPort)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to initialize service discovery: %v\n", err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Register service
+ instanceID := uuid.New().String()
+ if err = discovery.Register(ctx, serviceName, instanceID, host, *httpPort, *grpcPort); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to register service: %v\n", err)
+ }
+
+ // Watch service
+ go watchInstances(ctx, discovery, serviceName, instanceID)
+
+ // Close
+ defer func() {
+ if err = discovery.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Service discovery close error: %v\n", err)
+ } else {
+ fmt.Fprintf(os.Stdout, "Service discovery closed successfully\n")
+ }
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+}
+
+// watchInstances ...
+func watchInstances(ctx context.Context, discovery servicediscovery.ServiceDiscovery, serviceName, instanceID string) {
+ if discovery == nil {
+ return
+ }
+
+ ch, err := discovery.Watch(ctx, serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to discovery watch: %v\n", err)
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ if discovery != nil {
+ if err = discovery.Deregister(ctx, serviceName, instanceID); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to deregister service: %v\n", err)
+ }
+ }
+ return
+ case instances, ok := <-ch:
+ if !ok {
+ return
+ }
+ instancesStr, _ := json.Marshal(instances)
+ fmt.Fprintf(os.Stdout, "Discovered instances: %d, list: %v \n", len(instances), string(instancesStr))
+ }
+ }
+}
+```
+
+
+
+## 服务发现 - Client 端
+下面是客户端服务发现和负载平衡代码的示例:
+```go
+var discovery *servicediscovery.DiscoveryWithLB
+
+// setupDiscovery .
+func setupDiscovery(serviceName string) error {
+ var err error
+ discovery, err = servicediscovery.NewDiscoveryWithLB(servicediscovery.Config{
+ //Type: servicediscovery.ServiceDiscoveryTypeEtcd,
+ //Addrs: "localhost:2379",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeConsul,
+ //Addrs: "localhost:8500",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeZookeeper,
+ //Addrs: "localhost:2181",
+
+ Type: servicediscovery.ServiceDiscoveryTypeNacos,
+ Addrs: "localhost:8848",
+ Username: "nacos",
+ Password: "nacos",
+
+ ServiceName: serviceName,
+ }, balancer.LoadBalancerTypeRoundRobin)
+ if err != nil {
+ return err
+ }
+
+ discovery.SetOutputLogCallback(func(logType servicediscovery.OutputLogType, message string) {
+ if logType == servicediscovery.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ return nil
+}
+
+// watchInstances ...
+func watchInstances(ctx context.Context, discovery servicediscovery.ServiceDiscovery, serviceName string) {
+ if discovery == nil {
+ return
+ }
+
+ ch, err := discovery.Watch(ctx, serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to discovery watch: %v\n", err)
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case instances, ok := <-ch:
+ if !ok {
+ return
+ }
+ instancesStr, _ := json.Marshal(instances)
+ fmt.Fprintf(os.Stdout, "Discovered instances: %d, list: %v \n", len(instances), string(instancesStr))
+ }
+ }
+}
+
+func selectUrl(serviceName string) string {
+ hostname, _ := os.Hostname()
+ inst, err := discovery.Select(serviceName, hostname)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to select instance: %v\n", err)
+ return ""
+ }
+ httpPort := inst.HTTPPort
+ return fmt.Sprintf("http://%s:%s/hello", inst.Host, httpPort)
+}
+
+func callRequests(serviceName string, numWorkers, requestsPerWorker int) {
+ wg := sync.WaitGroup{}
+ for i := 0; i < numWorkers; i++ {
+ wg.Add(1)
+ go func(workerID int) {
+ defer wg.Done()
+ for j := 0; j < requestsPerWorker; j++ {
+ url := selectUrl(serviceName)
+ fmt.Fprintf(os.Stdout, "worker: %d, request: %d selectUrl: %v\n", workerID, j, url)
+ time.Sleep(10 * time.Millisecond)
+ }
+ }(i)
+ }
+
+ wg.Wait()
+}
+
+func main() {
+ numWorkers := flag.Int("worker", 5, "Port for HTTP server")
+ requestsPerWorker := flag.Int("request", 10, "Port for gRPC server")
+ flag.Parse()
+
+ serviceName := "hello-app"
+ err := setupDiscovery(serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to initialize service discovery: %v\n", err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ go watchInstances(ctx, discovery, serviceName)
+
+ // Close
+ defer func() {
+ if err = discovery.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Service discovery close error: %v\n", err)
+ } else {
+ fmt.Fprintf(os.Stdout, "Service discovery closed successfully\n")
+ }
+ }()
+
+ fmt.Println(">>>>>>> string call request ...")
+ go func() {
+ for {
+ callRequests(serviceName, *numWorkers, *requestsPerWorker)
+ time.Sleep(1 * time.Second)
+ }
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+}
+```
+
+
+
+
+
+
+## 服务动态配置管理
+
+> 同步机制:自动根据 Version 版本控制更新,当配置中心的版本号大于本地配置版本时,会自动同步到本地;反之则会同步到配置中心。
+
+下面是服务动态配置使用的代码的示例:
+
+```go
+func main() {
+ configs := map[string]*provider.Config{
+ "/config/my-app/main": {
+ Name: "my-service-app-main",
+ //Version: 0,
+ Version: 2676136267083311000,
+ Content: `{"AppName": "my-app-main", "Port": 8081, DebugMode: false }`,
+ ValidateCallback: func(config *provider.Config) (skip bool, err error) {
+ if config.Content == "" {
+ return false, fmt.Errorf("contnet must be not empty")
+ }
+ return true, nil
+ },
+ },
+ "/config/my-app/db": {
+ Name: "my-service-app-db",
+ //Version: 0,
+ Version: 2676136267083311000,
+ Content: `{"AppName": "my-app-db", "Port": 3306 }`,
+ ValidateCallback: func(config *provider.Config) (skip bool, err error) {
+ if config.Content == "" {
+ return false, fmt.Errorf("contnet must be not empty")
+ }
+ return true, nil
+ },
+ },
+ }
+
+ providerCfg := provider.ProviderConfig{
+ //Type: provider.ProviderTypeEtcd,
+ //Endpoints: []string{"localhost:2379"},
+
+ //Type: provider.ProviderTypeConsul,
+ //Endpoints: []string{"localhost:8500"},
+
+ //Type: provider.ProviderTypeZookeeper,
+ //Endpoints: []string{"localhost:2181"},
+
+ Type: provider.ProviderTypeNacos,
+ Endpoints: []string{"localhost:8848"},
+ Username: "nacos",
+ Password: "nacos",
+ //NacosProviderConfig: provider.NacosProviderConfig{
+ // NacosExtraConfig: extraconfig.NacosExtraConfig{
+ // NamespaceId: "",
+ // },
+ //},
+ }
+
+ manager, err := dynaconfig.NewConfigManager(dynaconfig.ConfigManagerParams{
+ ProviderConfig: providerCfg,
+ Configs: configs,
+ })
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to create config mananger, err: %v \n", err)
+ }
+ manager.SetOutputLogCallback(func(logType dynaconfig.OutputLogType, message string) {
+ if logType == dynaconfig.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == dynaconfig.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == dynaconfig.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ log.Println(">>>>>>>>>>>> Hot reload triggered", "key", key, "content", config.Content)
+ if key == "/config/my-app/db" {
+ if helper.IsOnlyEmpty(config.Content) {
+ return errors.New("invalid port number")
+ }
+ fmt.Fprintf(os.Stderr, ">>>>>>>>>>>>>>> Reinitializing database connection, content: %v \n", config.Content)
+ }
+ return nil
+ })
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ if key == "/config/my-app/main" {
+ // test panic
+ //panic("Simulated panic in callback")
+ }
+ return nil
+ })
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ manager.ASyncConfig(ctx)
+ //if err = manager.SyncConfig(context.Background()); err != nil {
+ // fmt.Fprintf(os.Stderr, "Failed to sync config: %v\n", err)
+ // return
+ //}
+
+ if err = manager.Watch(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to start watch: %v \n", err)
+ return
+ }
+
+ defer func() {
+ if err = manager.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to close: %v \n", err)
+ }
+ }()
+
+ //////////////////////// testing /////////////////////////
+ // Testing read the configuration content in real time
+ go func() {
+ for {
+ time.Sleep(3 * time.Second)
+ for _, key := range []string{"/config/my-app/main", "/config/my-app/db"} {
+ config := manager.GetLocalConfig(key)
+ fmt.Printf("+++++++ >>> Current config for %s: %+v\n", key, config)
+ }
+ }
+ }()
+ /////////////////////////////////////////////////
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+
+}
+```
+
+## LICENSE
+MIT
+
+
\ No newline at end of file
diff --git a/__example/dynamic-config/main.go b/__example/dynamic-config/main.go
new file mode 100644
index 0000000..67eb9fa
--- /dev/null
+++ b/__example/dynamic-config/main.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/wenlng/go-service-link/dynaconfig"
+ "github.com/wenlng/go-service-link/dynaconfig/provider"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+func main() {
+ configs := map[string]*provider.Config{
+ "/config/my-app/main": {
+ Name: "my-service-app-main",
+ //Version: 0,
+ Version: 2676136267083311000,
+ Content: `{"AppName": "my-app-main", "Port": 8081, DebugMode: false }`,
+ ValidateCallback: func(config *provider.Config) (skip bool, err error) {
+ if config.Content == "" {
+ return false, fmt.Errorf("contnet must be not empty")
+ }
+ return true, nil
+ },
+ },
+ "/config/my-app/db": {
+ Name: "my-service-app-db",
+ //Version: 0,
+ Version: 2676136267083311000,
+ Content: `{"AppName": "my-app-db", "Port": 3306 }`,
+ ValidateCallback: func(config *provider.Config) (skip bool, err error) {
+ if config.Content == "" {
+ return false, fmt.Errorf("contnet must be not empty")
+ }
+ return true, nil
+ },
+ },
+ }
+
+ providerCfg := provider.ProviderConfig{
+ //Type: provider.ProviderTypeEtcd,
+ //Endpoints: []string{"localhost:2379"},
+
+ //Type: provider.ProviderTypeConsul,
+ //Endpoints: []string{"localhost:8500"},
+
+ //Type: provider.ProviderTypeZookeeper,
+ //Endpoints: []string{"localhost:2181"},
+
+ Type: provider.ProviderTypeNacos,
+ Endpoints: []string{"localhost:8848"},
+ Username: "nacos",
+ Password: "nacos",
+ //NacosProviderConfig: provider.NacosProviderConfig{
+ // NacosExtraConfig: extraconfig.NacosExtraConfig{
+ // NamespaceId: "",
+ // },
+ //},
+ }
+
+ manager, err := dynaconfig.NewConfigManager(dynaconfig.ConfigManagerParams{
+ ProviderConfig: providerCfg,
+ Configs: configs,
+ })
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to create config mananger, err: %v \n", err)
+ }
+ manager.SetOutputLogCallback(func(logType dynaconfig.OutputLogType, message string) {
+ if logType == dynaconfig.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == dynaconfig.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == dynaconfig.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ log.Println(">>>>>>>>>>>> Hot reload triggered", "key", key, "content", config.Content)
+ if key == "/config/my-app/db" {
+ if helper.IsOnlyEmpty(config.Content) {
+ return errors.New("invalid port number")
+ }
+ fmt.Fprintf(os.Stderr, ">>>>>>>>>>>>>>> Reinitializing database connection, content: %v \n", config.Content)
+ }
+ return nil
+ })
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ if key == "/config/my-app/main" {
+ // test panic
+ //panic("Simulated panic in callback")
+ }
+ return nil
+ })
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ manager.ASyncConfig(ctx)
+ //if err = manager.SyncConfig(context.Background()); err != nil {
+ // fmt.Fprintf(os.Stderr, "Failed to sync config: %v\n", err)
+ // return
+ //}
+
+ if err = manager.Watch(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to start watch: %v \n", err)
+ return
+ }
+
+ defer func() {
+ if err = manager.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to close: %v \n", err)
+ }
+ }()
+
+ //////////////////////// testing /////////////////////////
+ // Testing read the configuration content in real time
+ go func() {
+ for {
+ time.Sleep(3 * time.Second)
+ for _, key := range []string{"/config/my-app/main", "/config/my-app/db"} {
+ config := manager.GetLocalConfig(key)
+ fmt.Printf("+++++++ >>> Current config for %s: %+v\n", key, config)
+ }
+ }
+ }()
+ /////////////////////////////////////////////////
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+
+}
diff --git a/__example/service-discovery/client/main.go b/__example/service-discovery/client/main.go
new file mode 100644
index 0000000..842fe53
--- /dev/null
+++ b/__example/service-discovery/client/main.go
@@ -0,0 +1,153 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/wenlng/go-service-link/servicediscovery"
+ "github.com/wenlng/go-service-link/servicediscovery/balancer"
+)
+
+var discovery *servicediscovery.DiscoveryWithLB
+
+// setupDiscovery .
+func setupDiscovery(serviceName string) error {
+ var err error
+ discovery, err = servicediscovery.NewDiscoveryWithLB(servicediscovery.Config{
+ //Type: servicediscovery.ServiceDiscoveryTypeEtcd,
+ //Addrs: "localhost:2379",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeConsul,
+ //Addrs: "localhost:8500",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeZookeeper,
+ //Addrs: "localhost:2181",
+
+ Type: servicediscovery.ServiceDiscoveryTypeNacos,
+ Addrs: "localhost:8848",
+ Username: "nacos",
+ Password: "nacos",
+
+ ServiceName: serviceName,
+ }, balancer.LoadBalancerTypeRoundRobin)
+ if err != nil {
+ return err
+ }
+
+ discovery.SetOutputLogCallback(func(logType servicediscovery.OutputLogType, message string) {
+ if logType == servicediscovery.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ return nil
+}
+
+// watchInstances ...
+func watchInstances(ctx context.Context, discovery servicediscovery.ServiceDiscovery, serviceName string) {
+ if discovery == nil {
+ return
+ }
+
+ ch, err := discovery.Watch(ctx, serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to discovery watch: %v\n", err)
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case instances, ok := <-ch:
+ if !ok {
+ return
+ }
+ instancesStr, _ := json.Marshal(instances)
+ fmt.Fprintf(os.Stdout, "Discovered instances: %d, list: %v \n", len(instances), string(instancesStr))
+ }
+ }
+}
+
+func selectUrl(serviceName string) string {
+ hostname, _ := os.Hostname()
+ inst, err := discovery.Select(serviceName, hostname)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to select instance: %v\n", err)
+ return ""
+ }
+ httpPort := inst.HTTPPort
+ return fmt.Sprintf("http://%s:%s/hello", inst.Host, httpPort)
+}
+
+func callRequests(serviceName string, numWorkers, requestsPerWorker int) {
+ wg := sync.WaitGroup{}
+ for i := 0; i < numWorkers; i++ {
+ wg.Add(1)
+ go func(workerID int) {
+ defer wg.Done()
+ for j := 0; j < requestsPerWorker; j++ {
+ url := selectUrl(serviceName)
+ fmt.Fprintf(os.Stdout, "worker: %d, request: %d selectUrl: %v\n", workerID, j, url)
+ time.Sleep(10 * time.Millisecond)
+ }
+ }(i)
+ }
+
+ wg.Wait()
+}
+
+func main() {
+ numWorkers := flag.Int("worker", 5, "Port for HTTP server")
+ requestsPerWorker := flag.Int("request", 10, "Port for gRPC server")
+ flag.Parse()
+
+ serviceName := "hello-app"
+ err := setupDiscovery(serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to initialize service discovery: %v\n", err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ go watchInstances(ctx, discovery, serviceName)
+
+ // Close
+ defer func() {
+ if err = discovery.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Service discovery close error: %v\n", err)
+ } else {
+ fmt.Fprintf(os.Stdout, "Service discovery closed successfully\n")
+ }
+ }()
+
+ fmt.Println(">>>>>>> string call request ...")
+ go func() {
+ for {
+ callRequests(serviceName, *numWorkers, *requestsPerWorker)
+ time.Sleep(1 * time.Second)
+ }
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+}
diff --git a/__example/service-discovery/service/main.go b/__example/service-discovery/service/main.go
new file mode 100644
index 0000000..b7f8393
--- /dev/null
+++ b/__example/service-discovery/service/main.go
@@ -0,0 +1,130 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/google/uuid"
+ "github.com/wenlng/go-service-link/servicediscovery"
+)
+
+var discovery servicediscovery.ServiceDiscovery
+
+// setupDiscovery .
+func setupDiscovery(serviceName, httPort, grpcPort string) error {
+ var err error
+ discovery, err = servicediscovery.NewServiceDiscovery(servicediscovery.Config{
+ //Type: servicediscovery.ServiceDiscoveryTypeEtcd,
+ //Addrs: "localhost:2379",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeConsul,
+ //Addrs: "localhost:8500",
+
+ //Type: servicediscovery.ServiceDiscoveryTypeZookeeper,
+ //Addrs: "localhost:2181",
+
+ Type: servicediscovery.ServiceDiscoveryTypeNacos,
+ Addrs: "localhost:8848",
+ Username: "nacos",
+ Password: "nacos",
+
+ ServiceName: serviceName,
+ })
+ if err != nil {
+ return err
+ }
+
+ discovery.SetOutputLogCallback(func(logType servicediscovery.OutputLogType, message string) {
+ if logType == servicediscovery.OutputLogTypeError {
+ fmt.Fprintf(os.Stderr, "ERROR - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeWarn {
+ fmt.Fprintf(os.Stdout, "WARN - "+message+"\n")
+ } else if logType == servicediscovery.OutputLogTypeDebug {
+ fmt.Fprintf(os.Stdout, "DEBUG - "+message+"\n")
+ } else {
+ fmt.Fprintf(os.Stdout, "INFO -"+message+"\n")
+ }
+ })
+
+ return nil
+}
+
+func main() {
+ httpPort := flag.String("http-port", "8001", "Port for HTTP server")
+ grpcPort := flag.String("grpc-port", "9001", "Port for gRPC server")
+ flag.Parse()
+
+ serviceName := "hello-app"
+ host := "localhost"
+
+ err := setupDiscovery(serviceName, *httpPort, *grpcPort)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to initialize service discovery: %v\n", err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Register service
+ instanceID := uuid.New().String()
+ if err = discovery.Register(ctx, serviceName, instanceID, host, *httpPort, *grpcPort); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to register service: %v\n", err)
+ }
+
+ // Watch service
+ go watchInstances(ctx, discovery, serviceName, instanceID)
+
+ // Close
+ defer func() {
+ if err = discovery.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Service discovery close error: %v\n", err)
+ } else {
+ fmt.Fprintf(os.Stdout, "Service discovery closed successfully\n")
+ }
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+}
+
+// watchInstances ...
+func watchInstances(ctx context.Context, discovery servicediscovery.ServiceDiscovery, serviceName, instanceID string) {
+ if discovery == nil {
+ return
+ }
+
+ ch, err := discovery.Watch(ctx, serviceName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to discovery watch: %v\n", err)
+ }
+
+ for {
+ select {
+ case <-ctx.Done():
+ if discovery != nil {
+ if err = discovery.Deregister(ctx, serviceName, instanceID); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to deregister service: %v\n", err)
+ }
+ }
+ return
+ case instances, ok := <-ch:
+ if !ok {
+ return
+ }
+ instancesStr, _ := json.Marshal(instances)
+ fmt.Fprintf(os.Stdout, "Discovered instances: %d, list: %v \n", len(instances), string(instancesStr))
+ }
+ }
+}
diff --git a/dev/config/etcd/etcd.yaml b/dev/config/etcd/etcd.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml
new file mode 100644
index 0000000..9fc1b20
--- /dev/null
+++ b/dev/docker-compose.yml
@@ -0,0 +1,108 @@
+version: '3'
+services:
+ zookeeper:
+ image: zookeeper:3.8
+ restart: always
+ container_name: zookeeper
+ ports:
+ - "2181:2181"
+ environment:
+ - ZOO_MY_ID=1
+ - ZOO_SERVERS=server.1=0.0.0.0:2888:3888;2181
+ volumes:
+ - ./store/zookeeper/data:/data
+ - ./store/zookeeper/zookeeper-datalog:/datalog
+ networks:
+ - local_net
+ healthcheck:
+ test: [ "CMD", "echo", "ruok", "|", "nc", "localhost", "2181" ]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ etcd:
+ container_name: etcd
+ image: bitnami/etcd:3.5.7
+ deploy:
+ replicas: 1
+ restart_policy:
+ condition: on-failure
+ environment:
+ - ALLOW_NONE_AUTHENTICATION=yes
+ privileged: true
+ volumes:
+ - ./store/etcd/data:/bitnami/etcd/data
+ ports:
+ - "2379:2379"
+ - "2380:2380"
+ networks:
+ - local_net
+
+ redis:
+ container_name: redis
+ image: redis:7.2.3
+ restart: always
+ volumes:
+ - ./store/redis/data:/data
+ - ./config/redis/redis.conf:/etc/redis/redis.conf
+ ports:
+ - "6379:6379"
+ environment:
+ TZ: Asia/Shanghai
+ command: redis-server /etc/redis/redis.conf
+ networks:
+ - local_net
+
+ nacos:
+ image: nacos/nacos-server:v2.3.2
+ container_name: nacos
+ restart: always
+ ports:
+ - "8848:8848"
+ - "9848:9848"
+ environment:
+ - MODE=standalone
+ - JVM_XMS=512m
+ - JVM_XMX=512m
+ - NACOS_AUTH_ENABLE=true
+ - NACOS_SERVER_PORT=8848
+ - NACOS_AUTH_IDENTITY_KEY=d72768e49f60cf39c86aefcd63106262
+ - NACOS_AUTH_IDENTITY_VALUE=7e671101b7b872350e5829ba7a3c2c3a
+ - NACOS_AUTH_TOKEN=MT1TjuOgGRUFIM7rpsGXaux+bjM4WDNXohKr1UJKDxk=
+# - NACOS_AUTH_ENABLE_USERAGENT_AUTH_WHITE=true # dev env
+ volumes:
+ - ./store/nacos-logs:/home/nacos/logs
+ - ./store/nacos-data:/home/nacos/data
+ networks:
+ - local_net
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8848/nacos/actuator/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ consul:
+ image: hashicorp/consul:latest
+ container_name: consul
+ platform: linux/amd64
+ restart: always
+ ports:
+ - "8500:8500"
+ - "8600:8600/udp"
+ command: ["consul", "agent", "-server", "-bootstrap-expect=1", "-ui", "-client=0.0.0.0", "-data-dir=/consul/data"]
+ volumes:
+ - ./store/consul-data:/consul/data
+ networks:
+ - local_net
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8500/v1/status/leader"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+networks:
+ local_net:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.20.0.0/16
\ No newline at end of file
diff --git a/dev/test.go b/dev/test.go
new file mode 100644
index 0000000..82ffb71
--- /dev/null
+++ b/dev/test.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/nacos-group/nacos-sdk-go/v2/clients"
+ "github.com/nacos-group/nacos-sdk-go/v2/common/constant"
+ "github.com/nacos-group/nacos-sdk-go/v2/model"
+ "github.com/nacos-group/nacos-sdk-go/v2/vo"
+)
+
+func main() {
+ client, err := clients.NewNamingClient(vo.NacosClientParam{
+ ClientConfig: &constant.ClientConfig{
+ NamespaceId: "",
+ Username: "nacos",
+ Password: "nacos",
+ },
+ ServerConfigs: []constant.ServerConfig{
+ {IpAddr: "localhost", Port: 8848},
+ },
+ })
+ if err != nil {
+ fmt.Printf("Failed to create client: %v\n", err)
+ return
+ }
+
+ success, err := client.RegisterInstance(vo.RegisterInstanceParam{
+ Ip: "127.0.0.1",
+ Port: 8080,
+ ServiceName: "hello-app",
+ Weight: 1,
+ Enable: true,
+ Healthy: true,
+ Ephemeral: true,
+ })
+ fmt.Printf("RegisterInstance: success=%v, err=%v\n", success, err)
+
+ subscribeParam := &vo.SubscribeParam{
+ ServiceName: "hello-app",
+ SubscribeCallback: func(services []model.Instance, err error) {
+ fmt.Println("services >>>>>>>>>>>", services)
+ },
+ }
+
+ err = client.Subscribe(subscribeParam)
+ if err != nil {
+ fmt.Println("sub err >>>>>>>>>>>", err)
+ }
+
+ defer func() {
+ _ = client.Unsubscribe(subscribeParam)
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ fmt.Println("Press Ctrl+C to exit...")
+ <-sigCh
+
+ fmt.Println("\nReceived shutdown signal. Exiting...")
+ os.Exit(0)
+}
diff --git a/dynaconfig/config_manager.go b/dynaconfig/config_manager.go
new file mode 100644
index 0000000..ad8509e
--- /dev/null
+++ b/dynaconfig/config_manager.go
@@ -0,0 +1,345 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package dynaconfig
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/wenlng/go-service-link/dynaconfig/provider"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+// OutputLogType ..
+type OutputLogType = helper.OutputLogType
+
+const (
+ OutputLogTypeWarn = helper.OutputLogTypeWarn
+ OutputLogTypeInfo = helper.OutputLogTypeInfo
+ OutputLogTypeError = helper.OutputLogTypeError
+ OutputLogTypeDebug = helper.OutputLogTypeDebug
+)
+
+// ReloadCallback ..
+type ReloadCallback func(key string, config *provider.Config) error
+
+type OutputLogCallback = helper.OutputLogCallback
+
+// ConfigManager manage multi-configuration synchronization, listening and hot loading
+type ConfigManager struct {
+ provider provider.ConfigProvider
+ configs map[string]*provider.Config
+ keys []string
+ callbacks []ReloadCallback
+ mu sync.RWMutex
+ wg sync.WaitGroup
+ ctx context.Context
+ cancel context.CancelFunc
+ healthFreq time.Duration // Health checkup frequency
+ healthCache provider.HealthStatus // Health check cache
+ healthCacheT time.Time // Cache time
+ cacheMu sync.RWMutex // Cache lock
+ cbsMu sync.RWMutex // Callback lock
+
+ outputLogCallback OutputLogCallback
+}
+
+// ConfigManagerParams ..
+type ConfigManagerParams struct {
+ ProviderConfig provider.ProviderConfig
+ Configs map[string]*provider.Config
+}
+
+// NewConfigManager ..
+func NewConfigManager(cmp ConfigManagerParams) (*ConfigManager, error) {
+ ctx, cancel := context.WithCancel(context.Background())
+
+ p, err := provider.NewProvider(cmp.ProviderConfig)
+ if err != nil {
+ cancel()
+ return nil, fmt.Errorf("failed to create provider, err: %v", err)
+ }
+
+ keys := make([]string, 0)
+ for key, _ := range cmp.Configs {
+ keys = append(keys, key)
+ }
+
+ m := &ConfigManager{
+ provider: p,
+ configs: cmp.Configs,
+ keys: keys,
+ ctx: ctx,
+ cancel: cancel,
+ healthFreq: 10 * time.Second,
+ }
+ m.startHealthCheck()
+ return m, nil
+}
+
+// SetOutputLogCallback Set the log out hook function
+func (m *ConfigManager) SetOutputLogCallback(outputLogCallback OutputLogCallback) {
+ m.provider.SetOutputLogCallback(outputLogCallback)
+ m.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (m *ConfigManager) outLog(logType helper.OutputLogType, message string) {
+ if m.outputLogCallback != nil {
+ m.outputLogCallback(logType, message)
+ }
+}
+
+// Subscribe to the hot loading callback for configuration changes
+func (m *ConfigManager) Subscribe(callback ReloadCallback) {
+ m.cbsMu.Lock()
+ defer m.cbsMu.Unlock()
+ m.callbacks = append(m.callbacks, callback)
+}
+
+// ASyncConfig synchronous configuration
+func (m *ConfigManager) ASyncConfig(ctx context.Context) {
+ go func() {
+ err := m.SyncConfig(ctx)
+ if err != nil {
+ if m.outputLogCallback != nil {
+ m.outputLogCallback(helper.OutputLogTypeError, fmt.Sprintf("[ConfigManager] Async config err: %v", err))
+ }
+ }
+ }()
+}
+
+// SyncConfig synchronous configuration
+func (m *ConfigManager) SyncConfig(ctx context.Context) error {
+ for _, key := range m.keys {
+ remote, err := m.provider.GetConfig(ctx, key)
+ if err != nil {
+ return fmt.Errorf("failed to get config for key %s: %v", key, err)
+ }
+
+ m.mu.RLock()
+ localConfig := m.configs[key]
+ m.mu.RUnlock()
+
+ var skip bool
+
+ if remote == nil {
+ if skip, err = localConfig.CallValidate(); err != nil {
+ if skip {
+ continue
+ }
+
+ return fmt.Errorf("local config validation failed for key %s: %v", key, err)
+ }
+
+ if localConfig.Version <= 0 {
+ //localConfig.Version = time.Now().UnixNano()
+ localConfig.Version = 1
+ }
+
+ if err = m.provider.PutConfig(ctx, key, localConfig); err != nil {
+ return fmt.Errorf("failed to put config for key %s: %v", key, err)
+ }
+
+ m.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[ConfigManager] Uploaded local config, key: %v", key))
+ } else {
+ if skip, err = remote.CallValidate(); err != nil {
+ if skip {
+ continue
+ }
+ return fmt.Errorf("remote config validation failed for key %s: %v", key, err)
+ }
+
+ if remote.Version > localConfig.Version {
+ m.mu.Lock()
+ m.configs[key] = remote
+ m.mu.Unlock()
+
+ m.notifyCallbacks(key, remote)
+
+ m.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[ConfigManager] Synced remote config, key: %v", key))
+ } else if remote.Version < localConfig.Version {
+ if err = m.provider.PutConfig(ctx, key, localConfig); err != nil {
+ return fmt.Errorf("failed to put config for key %s: %v", key, err)
+ }
+
+ m.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[ConfigManager] Uploaded local config, key: %v", key))
+ }
+ }
+ }
+
+ return nil
+}
+
+// RefreshConfig sync refresh configuration
+func (m *ConfigManager) RefreshConfig(ctx context.Context, key string, p *provider.Config) {
+ if _, ok := m.configs[key]; !ok {
+ return
+ }
+
+ m.mu.Lock()
+ m.configs[key] = p
+ m.mu.Unlock()
+
+ m.ASyncConfig(ctx)
+}
+
+// Watch for configuration changes
+func (m *ConfigManager) Watch() error {
+ for _, k := range m.keys {
+ key := k
+ m.wg.Add(1)
+ go func() {
+ defer m.wg.Done()
+ for {
+ select {
+ case <-m.ctx.Done():
+ return
+ default:
+ err := m.provider.WatchConfig(m.ctx, key, func(config *provider.Config) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if skip, err := config.CallValidate(); err != nil {
+ if skip {
+ return
+ }
+
+ m.outLog(helper.OutputLogTypeError, fmt.Sprintf("[ConfigManager] Watched config validation failed, key: %v, err: %v", key, err))
+ return
+ }
+ if config.Version > m.configs[key].Version {
+ m.configs[key] = config
+ m.notifyCallbacks(key, config)
+
+ m.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[ConfigManager] Updated local config, key: %v, config: %v", key, config))
+ }
+ })
+
+ if err != nil {
+ status, ok := m.getCachedHealthStatus()
+ if !ok || status.Err != nil {
+ continue
+ }
+
+ if err = m.SyncConfig(m.ctx); err != nil {
+ m.outLog(helper.OutputLogTypeError, fmt.Sprintf("[ConfigManager] Resync failed after reconnect, key: %v", key))
+ }
+
+ return
+ } else {
+ return
+ }
+ }
+ }
+ }()
+ }
+ return nil
+}
+
+// notifyCallbacks notify all subscribers to handle the callback error
+func (m *ConfigManager) notifyCallbacks(key string, config *provider.Config) {
+ m.cbsMu.RLock()
+ defer m.cbsMu.RUnlock()
+ for i, callback := range m.callbacks {
+ go func(i int, cb ReloadCallback) {
+ defer func() {
+ if r := recover(); r != nil {
+ m.outLog(helper.OutputLogTypeError, fmt.Sprintf("[ConfigManager] Callback panicked, key: %v, err: %v", key, r))
+ }
+ }()
+ if err := cb(key, config); err != nil {
+ m.outLog(helper.OutputLogTypeError, fmt.Sprintf("[ConfigManager] Callback failed, key: %v, err: %v", key, err))
+ }
+ }(i, callback)
+ }
+}
+
+// getCachedHealthStatus obtain the cached health check results
+func (m *ConfigManager) getCachedHealthStatus() (provider.HealthStatus, bool) {
+ m.cacheMu.RLock()
+ defer m.cacheMu.RUnlock()
+
+ if time.Since(m.healthCacheT) < 5*time.Second {
+ return m.healthCache, true
+ }
+
+ return provider.HealthStatus{}, false
+}
+
+// setCachedHealthStatus set up the health check cache
+func (m *ConfigManager) setCachedHealthStatus(status provider.HealthStatus) {
+ m.cacheMu.Lock()
+ defer m.cacheMu.Unlock()
+
+ m.healthCache = status
+ m.healthCacheT = time.Now()
+}
+
+// startHealthCheck start the health check
+func (m *ConfigManager) startHealthCheck() {
+ m.wg.Add(1)
+ go func() {
+ defer m.wg.Done()
+ ticker := time.NewTicker(m.healthFreq)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-m.ctx.Done():
+ return
+ case <-ticker.C:
+ if status, ok := m.getCachedHealthStatus(); ok {
+ if status.Err == nil {
+ m.outLog(helper.OutputLogTypeDebug, fmt.Sprintf("[ConfigManager] Using cached health status, mertice: %v", status.Metrics))
+ continue
+ }
+ }
+
+ // Carry out health checks
+ ctx, cancel := context.WithTimeout(m.ctx, 5*time.Second)
+ status := m.provider.HealthCheck(ctx)
+ cancel()
+ m.setCachedHealthStatus(status)
+
+ if status.Err != nil {
+ m.outLog(helper.OutputLogTypeWarn, fmt.Sprintf("[ConfigManager] Health check failed, mertice: %v, err: %v", status.Metrics, status.Err))
+ m.healthFreq = 2 * time.Second // Increase the frequency when failure occurs
+ if err := m.SyncConfig(m.ctx); err != nil {
+ m.outLog(helper.OutputLogTypeError, fmt.Sprintf("[ConfigManager] Resync failed after reconnect, err: %v", err))
+ }
+ } else {
+ m.healthFreq = 10 * time.Second // Return to the normal frequency when successful
+ m.outLog(helper.OutputLogTypeDebug, fmt.Sprintf("[ConfigManager] Health check passed, metrics: %v", status.Metrics))
+ }
+ ticker.Reset(m.healthFreq)
+ }
+ }
+ }()
+}
+
+// GetLocalConfig obtain the local configuration
+func (m *ConfigManager) GetLocalConfig(key string) *provider.Config {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ return m.configs[key]
+}
+
+// Close ..
+func (m *ConfigManager) Close() error {
+ m.cancel()
+ m.wg.Wait()
+
+ if err := m.provider.Close(); err != nil {
+ m.outLog(helper.OutputLogTypeError, fmt.Sprintf("[ConfigManager] Failed to close provider, err: %v", err))
+ return err
+ }
+
+ m.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[ConfigManager] Config manager closed"))
+ return nil
+}
diff --git a/dynaconfig/config_manager_test.go b/dynaconfig/config_manager_test.go
new file mode 100644
index 0000000..8092503
--- /dev/null
+++ b/dynaconfig/config_manager_test.go
@@ -0,0 +1,82 @@
+package dynaconfig
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "testing"
+
+ "github.com/wenlng/go-service-link/dynaconfig/provider"
+)
+
+func TestConfigManager(t *testing.T) {
+ configs := map[string]*provider.Config{
+ "/config/my-app/main": {
+ Name: "my-service-app-main",
+ Version: 2256136267083311000,
+ Content: `{"AppName": "my-app-main", "Port": 8080, DebugMode: true }`,
+ },
+ "/config/my-app/db": {
+ Name: "my-service-app-db",
+ Version: 2245136267083311000,
+ Content: `{"AppName": "my-app-main", "Port": 3306 }`,
+ },
+ }
+
+ keys := make([]string, 0)
+ for key, _ := range configs {
+ keys = append(keys, key)
+ }
+
+ // "etcd", "consul", "zookeeper", "nacos"
+ providerCfg := provider.ProviderConfig{
+ Type: provider.ProviderTypeEtcd,
+ Endpoints: []string{"localhost:2379"},
+ }
+
+ p, err := provider.NewProvider(providerCfg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to create provider: %v \n", err)
+ return
+ }
+
+ manager := NewConfigManager(p, configs, keys)
+
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ log.Println("Hot reload triggered", "key", key, "content", config.Content)
+ if key == "/config/my-app/db" {
+ if len(config.Content) <= 0 {
+ return errors.New("invalid port number")
+ }
+ fmt.Fprintf(os.Stderr, "Reinitializing database connection, content: %v \n", config.Content)
+ }
+ return nil
+ })
+ manager.Subscribe(func(key string, config *provider.Config) error {
+ if key == "/config/my-app/main" {
+ panic("Simulated panic in callback")
+ }
+ return nil
+ })
+
+ if err = manager.SyncConfig(context.Background()); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to sync config: %v\n", err)
+ return
+ }
+
+ if err := manager.Watch(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to start watch: %v \n", err)
+ return
+ }
+
+ for _, key := range keys {
+ config := manager.GetLocalConfig(key)
+ fmt.Printf("Current config for %s: %+v\n", key, config)
+ }
+
+ if err := manager.Close(); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to close: %v \n", err)
+ }
+}
diff --git a/dynaconfig/provider/consul_provider.go b/dynaconfig/provider/consul_provider.go
new file mode 100644
index 0000000..461e2ba
--- /dev/null
+++ b/dynaconfig/provider/consul_provider.go
@@ -0,0 +1,223 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/hashicorp/consul/api"
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+// ConsulProvider implement the Consul configuration center
+type ConsulProvider struct {
+ pool *clientpool.ConsulPool
+ address []string
+ mu sync.Mutex
+
+ config ConsulProviderConfig
+ outputLogCallback helper.OutputLogCallback
+}
+
+// ConsulProviderConfig ..
+type ConsulProviderConfig struct {
+ address []string
+ poolSize int
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+
+ ConsulExtraConfig extraconfig.ConsulExtraConfig
+}
+
+// NewConsulProvider ..
+func NewConsulProvider(conf ConsulProviderConfig) (ConfigProvider, error) {
+ if conf.poolSize <= 0 {
+ conf.poolSize = 5
+ }
+
+ if conf.username != "" {
+ conf.ConsulExtraConfig.Username = conf.username
+ }
+ if conf.password != "" {
+ conf.ConsulExtraConfig.Password = conf.password
+ }
+ if conf.tlsConfig != nil {
+ conf.ConsulExtraConfig.SetTLSConfig(conf.tlsConfig)
+ }
+
+ pool, err := clientpool.NewConsulPool(conf.poolSize, conf.address, &conf.ConsulExtraConfig)
+ if err != nil {
+ return nil, err
+ }
+ return &ConsulProvider{pool: pool, address: conf.address, config: conf}, nil
+}
+
+// SetOutputLogCallback Set the log out hook function
+func (p *ConsulProvider) SetOutputLogCallback(outputLogCallback helper.OutputLogCallback) {
+ p.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (p *ConsulProvider) outLog(logType helper.OutputLogType, message string) {
+ if p.outputLogCallback != nil {
+ p.outputLogCallback(logType, message)
+ }
+}
+
+// GetConfig ..
+func (p *ConsulProvider) GetConfig(ctx context.Context, key string) (*Config, error) {
+ key = strings.TrimPrefix(key, "/")
+ cli := p.pool.Get()
+ defer p.pool.Put(cli)
+
+ var config *Config
+ operation := func() error {
+ kv, _, err := cli.KV().Get(key, nil)
+ if err != nil {
+ return err
+ }
+ if kv == nil {
+ config = nil
+ return nil
+ }
+ config = &Config{}
+ return json.Unmarshal(kv.Value, config)
+ }
+
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ return nil, fmt.Errorf("failed to get config: %v", err)
+ }
+ return config, nil
+}
+
+// PutConfig ..
+func (p *ConsulProvider) PutConfig(ctx context.Context, key string, config *Config) error {
+ key = strings.TrimPrefix(key, "/")
+ cli := p.pool.Get()
+ defer p.pool.Put(cli)
+
+ data, err := json.Marshal(config)
+ if err != nil {
+ return fmt.Errorf("failed to marshal config: %v", err)
+ }
+
+ lockKey := strings.TrimPrefix(key, "/") + "_lock"
+ lock, err := cli.LockKey(lockKey)
+ if err != nil {
+ return fmt.Errorf("failed to create consul lock: %v", err)
+ }
+ defer lock.Unlock()
+
+ if _, err = lock.Lock(nil); err != nil {
+ return fmt.Errorf("failed to acquire consul lock: %v", err)
+ }
+
+ operation := func() error {
+ _, err = cli.KV().Put(&api.KVPair{Key: key, Value: data}, nil)
+ return err
+ }
+ if err = helper.WithRetry(ctx, operation); err != nil {
+ return fmt.Errorf("failed to put config: %v", err)
+ }
+
+ p.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[ConsulProvider] Config written with lock, key: %v", key))
+ return nil
+}
+
+// WatchConfig ..
+func (p *ConsulProvider) WatchConfig(ctx context.Context, key string, callback func(*Config)) error {
+ key = strings.TrimPrefix(key, "/")
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ cli := p.pool.Get()
+ kv, _, err := cli.KV().Get(key, nil)
+ if err != nil {
+ p.outLog(helper.OutputLogTypeWarn, fmt.Sprintf("[ConsulProvider] Consul watch error, key: %v", key))
+ p.pool.Put(cli)
+ time.Sleep(time.Second)
+ continue
+ }
+ if kv != nil {
+ var config Config
+ if err := json.Unmarshal(kv.Value, &config); err == nil {
+ callback(&config)
+ }
+ }
+ p.pool.Put(cli)
+ time.Sleep(time.Second)
+ }
+ }
+ }()
+ return nil
+}
+
+// HealthCheck ..
+func (p *ConsulProvider) HealthCheck(ctx context.Context) HealthStatus {
+ cli := p.pool.Get()
+ defer p.pool.Put(cli)
+ metrics := make(map[string]interface{})
+ start := time.Now()
+
+ // Check the status of the leader
+ leader, err := cli.Status().Leader()
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("consul health check failed: %v", err)}
+ }
+ metrics["latency_ms"] = time.Since(start).Milliseconds()
+ metrics["leader"] = leader
+
+ // Check the writing capability
+ testKey := "health/test"
+ _, err = cli.KV().Put(&api.KVPair{Key: testKey, Value: []byte("test")}, nil)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("consul write check failed: %v", err)}
+ }
+ _, err = cli.KV().Delete(testKey, nil)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("consul delete check failed: %v", err)}
+ }
+
+ // Check the monitoring interface
+ hUrl := helper.EnsureHTTP(p.address[0] + "/v1/health/service/consul")
+ respHTTP, err := http.Get(hUrl)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("consul health endpoint failed: %v", err)}
+ }
+ defer respHTTP.Body.Close()
+ if respHTTP.StatusCode != http.StatusOK {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("consul health endpoint returned %d", respHTTP.StatusCode)}
+ }
+
+ // Check the status of the cluster
+ peers, err := cli.Status().Peers()
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("consul peers check failed: %v", err)}
+ }
+ metrics["peers"] = len(peers)
+ return HealthStatus{Metrics: metrics}
+}
+
+// Close ..
+func (p *ConsulProvider) Close() error {
+ p.pool.Close()
+ return nil
+}
diff --git a/dynaconfig/provider/etcd_provider.go b/dynaconfig/provider/etcd_provider.go
new file mode 100644
index 0000000..03e3df0
--- /dev/null
+++ b/dynaconfig/provider/etcd_provider.go
@@ -0,0 +1,230 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+ "go.etcd.io/etcd/client/v3"
+ "go.etcd.io/etcd/client/v3/concurrency"
+)
+
+// EtcdProvider implement the Etcd configuration center
+type EtcdProvider struct {
+ pool *clientpool.EtcdPool
+ endpoints []string
+ mu sync.Mutex
+
+ config EtcdProviderConfig
+ outputLogCallback helper.OutputLogCallback
+}
+
+// EtcdProviderConfig ..
+type EtcdProviderConfig struct {
+ address []string
+ poolSize int
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+
+ EtcdExtraConfig extraconfig.EtcdExtraConfig
+}
+
+// NewEtcdProvider ..
+func NewEtcdProvider(conf EtcdProviderConfig) (ConfigProvider, error) {
+ if conf.poolSize <= 0 {
+ conf.poolSize = 5
+ }
+
+ if conf.username != "" {
+ conf.EtcdExtraConfig.Username = conf.username
+ }
+ if conf.password != "" {
+ conf.EtcdExtraConfig.Password = conf.password
+ }
+ if conf.tlsConfig != nil {
+ conf.EtcdExtraConfig.SetTLSConfig(conf.tlsConfig)
+ }
+
+ pool, err := clientpool.NewEtcdPool(conf.poolSize, conf.address, &conf.EtcdExtraConfig)
+ if err != nil {
+ return nil, err
+ }
+ return &EtcdProvider{pool: pool, endpoints: conf.address, config: conf}, nil
+}
+
+// SetOutputLogCallback Set the log out hook function
+func (p *EtcdProvider) SetOutputLogCallback(outputLogCallback helper.OutputLogCallback) {
+ p.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (p *EtcdProvider) outLog(logType helper.OutputLogType, message string) {
+ if p.outputLogCallback != nil {
+ p.outputLogCallback(logType, message)
+ }
+}
+
+// GetConfig ..
+func (p *EtcdProvider) GetConfig(ctx context.Context, key string) (*Config, error) {
+ cli := p.pool.Get()
+ defer p.pool.Put(cli)
+
+ var config *Config
+ operation := func() error {
+ resp, err := cli.Get(ctx, key)
+ if err != nil {
+ return err
+ }
+ if len(resp.Kvs) == 0 {
+ config = nil
+ return nil
+ }
+ config = &Config{}
+ return json.Unmarshal(resp.Kvs[0].Value, config)
+ }
+
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ return nil, fmt.Errorf("failed to get config: %v", err)
+ }
+ return config, nil
+}
+
+// PutConfig ..
+func (p *EtcdProvider) PutConfig(ctx context.Context, key string, config *Config) error {
+ cli := p.pool.Get()
+ defer p.pool.Put(cli)
+
+ data, err := json.Marshal(config)
+ if err != nil {
+ return fmt.Errorf("failed to marshal config: %v", err)
+ }
+
+ session, err := concurrency.NewSession(cli, concurrency.WithTTL(5))
+ if err != nil {
+ return fmt.Errorf("failed to create etcd session: %v", err)
+ }
+ defer session.Close()
+
+ lockKey := key + "_lock"
+ mutex := concurrency.NewMutex(session, lockKey)
+ if err = mutex.Lock(ctx); err != nil {
+ return fmt.Errorf("failed to acquire etcd lock: %v", err)
+ }
+ defer mutex.Unlock(ctx)
+
+ operation := func() error {
+ _, err = cli.Put(ctx, key, string(data))
+ return err
+ }
+ if err = helper.WithRetry(ctx, operation); err != nil {
+ return fmt.Errorf("failed to put config: %v", err)
+ }
+
+ p.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[EtcdProvider] Config written with lock, key: %v", key))
+ return nil
+}
+
+// WatchConfig ..
+func (p *EtcdProvider) WatchConfig(ctx context.Context, key string, callback func(*Config)) error {
+ cli := p.pool.Get()
+ defer p.pool.Put(cli)
+ watchChan := cli.Watch(ctx, key)
+
+ go func() {
+ for {
+ select {
+ case resp, ok := <-watchChan:
+ if !ok {
+ p.outLog(helper.OutputLogTypeWarn, fmt.Sprintf("[EtcdProvider] Etcd watch channel closed, key: %v", key))
+ return
+ }
+
+ for _, ev := range resp.Events {
+ if ev.Type == clientv3.EventTypePut {
+ var config Config
+ if err := json.Unmarshal(ev.Kv.Value, &config); err == nil {
+ callback(&config)
+ }
+ }
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ return nil
+}
+
+// HealthCheck ..
+func (p *EtcdProvider) HealthCheck(ctx context.Context) HealthStatus {
+ cli := p.pool.Get()
+ defer p.pool.Put(cli)
+ metrics := make(map[string]interface{})
+ start := time.Now()
+
+ // Check the writing capability
+ testKey := "/health/test"
+ _, err := cli.Put(ctx, testKey, "test")
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("etcd write check failed: %v", err)}
+ }
+
+ // Check the connection status
+ resp, err := cli.Get(ctx, "/health", clientv3.WithLimit(1))
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("etcd health check failed: %v", err)}
+ }
+ metrics["latency_ms"] = time.Since(start).Milliseconds()
+ metrics["node_count"] = len(resp.Kvs)
+
+ _, err = cli.Delete(ctx, testKey)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("etcd delete check failed: %v", err)}
+ }
+
+ // Check the monitoring interface
+ hUrl := helper.EnsureHTTP(p.endpoints[0] + "/health")
+ respHTTP, err := http.Get(hUrl)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("etcd health endpoint failed: %v", err)}
+ }
+ defer respHTTP.Body.Close()
+ if respHTTP.StatusCode != http.StatusOK {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("etcd health endpoint returned %d", respHTTP.StatusCode)}
+ }
+
+ // Check the status of the cluster
+ clusterResp, err := cli.Status(ctx, p.endpoints[0])
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("etcd cluster status check failed: %v", err)}
+ }
+ metrics["leader"] = clusterResp.Leader
+
+ // Obtain member information
+ memberResp, err := cli.MemberList(ctx)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("etcd member list check failed: %v", err)}
+ }
+ metrics["members"] = len(memberResp.Members)
+ return HealthStatus{Metrics: metrics}
+}
+
+// Close ..
+func (p *EtcdProvider) Close() error {
+ p.pool.Close()
+ return nil
+}
diff --git a/dynaconfig/provider/nacos_provider.go b/dynaconfig/provider/nacos_provider.go
new file mode 100644
index 0000000..d601212
--- /dev/null
+++ b/dynaconfig/provider/nacos_provider.go
@@ -0,0 +1,208 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/nacos-group/nacos-sdk-go/v2/vo"
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+// NacosProvider implement the Nacos configuration center
+type NacosProvider struct {
+ pool *clientpool.NacosConfigPool
+ serverAddrs []string
+ mu sync.Mutex
+
+ config NacosProviderConfig
+ outputLogCallback helper.OutputLogCallback
+}
+
+// NacosProviderConfig ..
+type NacosProviderConfig struct {
+ address []string
+ poolSize int
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+
+ NacosExtraConfig extraconfig.NacosExtraConfig
+}
+
+// NewNacosProvider ..
+func NewNacosProvider(conf NacosProviderConfig) (ConfigProvider, error) {
+ if conf.NacosExtraConfig.NamespaceId == "" {
+ conf.NacosExtraConfig.NamespaceId = ""
+ }
+ if conf.poolSize <= 0 {
+ conf.poolSize = 5
+ }
+ if conf.username != "" {
+ conf.NacosExtraConfig.Username = conf.username
+ }
+ if conf.password != "" {
+ conf.NacosExtraConfig.Password = conf.password
+ }
+ if conf.tlsConfig != nil {
+ conf.NacosExtraConfig.SetTLSConfig(conf.tlsConfig)
+ }
+
+ pool, err := clientpool.NewNacosConfigPool(conf.poolSize, conf.address, &conf.NacosExtraConfig)
+ if err != nil {
+ return nil, err
+ }
+ return &NacosProvider{pool: pool, serverAddrs: conf.address, config: conf}, nil
+}
+
+// SetOutputLogCallback Set the log out hook function
+func (p *NacosProvider) SetOutputLogCallback(outputLogCallback helper.OutputLogCallback) {
+ p.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (p *NacosProvider) outLog(logType helper.OutputLogType, message string) {
+ if p.outputLogCallback != nil {
+ p.outputLogCallback(logType, message)
+ }
+}
+
+// GetConfig ..
+func (p *NacosProvider) GetConfig(ctx context.Context, key string) (*Config, error) {
+ key = strings.TrimPrefix(key, "/")
+ key = strings.ReplaceAll(key, "/", ".")
+
+ client := p.pool.Get()
+ defer p.pool.Put(client)
+ var config *Config
+ operation := func() error {
+ content, err := client.GetConfig(vo.ConfigParam{DataId: key, Group: "DEFAULT_GROUP"})
+ if err != nil && !strings.Contains(err.Error(), "config data not exist") {
+ return err
+ }
+
+ if content == "" {
+ config = nil
+ return nil
+ }
+
+ config = &Config{}
+ return json.Unmarshal([]byte(content), config)
+ }
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ return nil, fmt.Errorf("failed to get config: %v", err)
+ }
+ return config, nil
+}
+
+// PutConfig ..
+func (p *NacosProvider) PutConfig(ctx context.Context, key string, config *Config) error {
+ key = strings.TrimPrefix(key, "/")
+ key = strings.ReplaceAll(key, "/", ".")
+
+ client := p.pool.Get()
+ defer p.pool.Put(client)
+
+ data, err := json.Marshal(config)
+ if err != nil {
+ return fmt.Errorf("failed to marshal config: %v", err)
+ }
+
+ operation := func() error {
+ _, err = client.PublishConfig(vo.ConfigParam{
+ DataId: key,
+ Group: "DEFAULT_GROUP",
+ Content: string(data),
+ })
+ return err
+ }
+ if err = helper.WithRetry(ctx, operation); err != nil {
+ return fmt.Errorf("failed to put config: %v", err)
+ }
+
+ p.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[NacosProvider] Config written, key: %v", key))
+ return nil
+}
+
+// WatchConfig ..
+func (p *NacosProvider) WatchConfig(ctx context.Context, key string, callback func(*Config)) error {
+ key = strings.TrimPrefix(key, "/")
+ key = strings.ReplaceAll(key, "/", ".")
+
+ client := p.pool.Get()
+ defer p.pool.Put(client)
+
+ err := client.ListenConfig(vo.ConfigParam{
+ DataId: key,
+ Group: "DEFAULT_GROUP",
+ OnChange: func(namespace, group, dataId, data string) {
+ var config Config
+ if err := json.Unmarshal([]byte(data), &config); err == nil {
+ callback(&config)
+ }
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("failed to watch config: %v", err)
+ }
+
+ return nil
+}
+
+// HealthCheck ..
+func (p *NacosProvider) HealthCheck(ctx context.Context) HealthStatus {
+ client := p.pool.Get()
+ defer p.pool.Put(client)
+ metrics := make(map[string]interface{})
+ start := time.Now()
+
+ // Check the writing capability
+ _, err := client.PublishConfig(vo.ConfigParam{DataId: "health.test", Group: "DEFAULT_GROUP", Content: "test"})
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("nacos write check failed: %v", err)}
+ }
+ metrics["latency_ms"] = time.Since(start).Milliseconds()
+
+ // Check the reading capability
+ _, err = client.GetConfig(vo.ConfigParam{DataId: "health.test", Group: "DEFAULT_GROUP"})
+ if err != nil && !strings.Contains(err.Error(), "config data not exist") {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("nacos health check failed: %v", err)}
+ }
+
+ // Check the deleting capability
+ _, err = client.DeleteConfig(vo.ConfigParam{DataId: "health.test", Group: "DEFAULT_GROUP"})
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("nacos delete check failed: %v", err)}
+ }
+
+ // Check the monitoring interface
+ hUrl := helper.EnsureHTTP(p.serverAddrs[0] + "/nacos/actuator/health")
+ respHTTP, err := http.Get(hUrl)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("nacos health endpoint failed: %v", err)}
+ }
+ defer respHTTP.Body.Close()
+ if respHTTP.StatusCode != http.StatusOK && respHTTP.StatusCode != http.StatusNotFound {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("nacos health endpoint returned %d", respHTTP.StatusCode)}
+ }
+ return HealthStatus{Metrics: metrics}
+}
+
+// Close ..
+func (p *NacosProvider) Close() error {
+ p.pool.Close()
+ return nil
+}
diff --git a/dynaconfig/provider/provider.go b/dynaconfig/provider/provider.go
new file mode 100644
index 0000000..c2220e0
--- /dev/null
+++ b/dynaconfig/provider/provider.go
@@ -0,0 +1,125 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+// ProviderType ..
+type ProviderType string
+
+// ProviderType .
+const (
+ ProviderTypeEtcd ProviderType = "etcd"
+ ProviderTypeZookeeper = "zookeeper"
+ ProviderTypeConsul = "consul"
+ ProviderTypeNacos = "nacos"
+ ProviderTypeNone = "none"
+)
+
+// Config define the configuration structure
+type Config struct {
+ Name string `json:"name"`
+ Version int64 `json:"version"`
+ Content interface{} `json:"content"`
+ ValidateCallback func(config *Config) (skip bool, err error) `json:"-"`
+}
+
+// CallValidate ...
+func (c *Config) CallValidate() (skip bool, err error) {
+ if c.ValidateCallback != nil {
+ return c.ValidateCallback(c)
+ }
+
+ if helper.IsOnlyEmpty(c.Content) {
+ return false, fmt.Errorf("content cannot be empty")
+ }
+ return false, nil
+}
+
+// HealthStatus define the results of health check-ups
+type HealthStatus struct {
+ Metrics map[string]interface{}
+ Err error
+}
+
+// ConfigProvider define the configuration center interface
+type ConfigProvider interface {
+ GetConfig(ctx context.Context, key string) (*Config, error)
+ PutConfig(ctx context.Context, key string, config *Config) error
+ WatchConfig(ctx context.Context, key string, callback func(*Config)) error
+ Close() error
+ HealthCheck(ctx context.Context) HealthStatus
+ SetOutputLogCallback(fn helper.OutputLogCallback)
+}
+
+// ProviderConfig ..
+type ProviderConfig struct {
+ Type ProviderType // "etcd", "consul", "zookeeper", "nacos"
+ Endpoints []string
+ Username string
+ Password string
+ PoolSize int
+ TlsConfig *common.TLSConfig
+
+ EtcdProviderConfig
+ NacosProviderConfig
+ ConsulProviderConfig
+ ZooKeeperProviderConfig
+}
+
+// NewProvider dynamically creates configuration centers
+func NewProvider(cfg ProviderConfig) (ConfigProvider, error) {
+ switch cfg.Type {
+ case ProviderTypeEtcd:
+ config := cfg.EtcdProviderConfig
+
+ config.address = cfg.Endpoints
+ config.tlsConfig = cfg.TlsConfig
+ config.username = cfg.Username
+ config.password = cfg.Password
+ config.poolSize = cfg.PoolSize
+ return NewEtcdProvider(config)
+ case ProviderTypeConsul:
+ config := cfg.ConsulProviderConfig
+
+ config.address = cfg.Endpoints
+ config.tlsConfig = cfg.TlsConfig
+ config.username = cfg.Username
+ config.password = cfg.Password
+ config.poolSize = cfg.PoolSize
+
+ return NewConsulProvider(config)
+ case ProviderTypeZookeeper:
+ config := cfg.ZooKeeperProviderConfig
+
+ config.address = cfg.Endpoints
+ config.tlsConfig = cfg.TlsConfig
+ config.username = cfg.Username
+ config.password = cfg.Password
+ config.poolSize = cfg.PoolSize
+
+ return NewZooKeeperProvider(config)
+ case ProviderTypeNacos:
+ config := cfg.NacosProviderConfig
+
+ config.address = cfg.Endpoints
+ config.tlsConfig = cfg.TlsConfig
+ config.username = cfg.Username
+ config.password = cfg.Password
+ config.poolSize = cfg.PoolSize
+
+ return NewNacosProvider(config)
+ default:
+ return nil, fmt.Errorf("unsupported provider type: %s", cfg.Type)
+ }
+}
diff --git a/dynaconfig/provider/zookeeper_provider.go b/dynaconfig/provider/zookeeper_provider.go
new file mode 100644
index 0000000..0122313
--- /dev/null
+++ b/dynaconfig/provider/zookeeper_provider.go
@@ -0,0 +1,271 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/go-zookeeper/zk"
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+// ZooKeeperProvider implement the ZookPeeper configuration center
+type ZooKeeperProvider struct {
+ pool *clientpool.ZooKeeperPool
+ address []string
+ mu sync.Mutex
+
+ config ZooKeeperProviderConfig
+ outputLogCallback helper.OutputLogCallback
+}
+
+// ZooKeeperProviderConfig ..
+type ZooKeeperProviderConfig struct {
+ address []string
+ poolSize int
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+
+ ZooKeeperExtraConfig extraconfig.ZooKeeperExtraConfig
+}
+
+// NewZooKeeperProvider ..
+func NewZooKeeperProvider(conf ZooKeeperProviderConfig) (ConfigProvider, error) {
+ if conf.poolSize <= 0 {
+ conf.poolSize = 3
+ }
+
+ if conf.username != "" {
+ conf.ZooKeeperExtraConfig.Username = conf.username
+ }
+ if conf.password != "" {
+ conf.ZooKeeperExtraConfig.Password = conf.password
+ }
+ if conf.tlsConfig != nil {
+ conf.ZooKeeperExtraConfig.SetTLSConfig(conf.tlsConfig)
+ }
+
+ zd := &ZooKeeperProvider{address: conf.address, config: conf}
+
+ zlogger := &extraconfig.Zlogger{
+ OutLogCallback: func(format string, s ...interface{}) {
+ if zd.outputLogCallback != nil {
+ zd.outputLogCallback(helper.OutputLogTypeInfo, fmt.Sprintf(format, s...))
+ }
+ },
+ }
+
+ excfg := &conf.ZooKeeperExtraConfig
+ excfg.SetZlogger(zlogger)
+
+ pool, err := clientpool.NewZooKeeperPool(conf.poolSize, conf.address, &conf.ZooKeeperExtraConfig)
+ if err != nil {
+ return nil, err
+ }
+ zd.pool = pool
+
+ return zd, nil
+}
+
+// SetOutputLogCallback Set the log out hook function
+func (p *ZooKeeperProvider) SetOutputLogCallback(outputLogCallback helper.OutputLogCallback) {
+ p.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (p *ZooKeeperProvider) outLog(logType helper.OutputLogType, message string) {
+ if p.outputLogCallback != nil {
+ p.outputLogCallback(logType, message)
+ }
+}
+
+// ensurePath ..
+func (p *ZooKeeperProvider) ensurePath(conn *zk.Conn, zkPath string) error {
+ parts := strings.Split(strings.Trim(zkPath, "/"), "/")
+ current := ""
+
+ for i, part := range parts[:len(parts)-1] {
+ current = current + "/" + part
+ if i == 0 {
+ current = "/" + part
+ }
+ exists, _, err := conn.Exists(current)
+ if err != nil {
+ return fmt.Errorf("failed to check path %s: %v", current, err)
+ }
+ if !exists {
+ _, err = conn.Create(current, []byte{}, 0, zk.WorldACL(zk.PermAll))
+ if err != nil && err != zk.ErrNodeExists {
+ return fmt.Errorf("failed to create path %s: %v", current, err)
+ }
+ }
+ }
+ return nil
+}
+
+// GetConfig ..
+func (p *ZooKeeperProvider) GetConfig(ctx context.Context, key string) (*Config, error) {
+ conn := p.pool.Get()
+ defer p.pool.Put(conn)
+ var config *Config
+
+ operation := func() error {
+ data, _, err := conn.Get(key)
+ if err != nil {
+ if err == zk.ErrNoNode {
+ config = nil
+ return nil
+ }
+ return err
+ }
+ config = &Config{}
+ return json.Unmarshal(data, config)
+ }
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ return nil, fmt.Errorf("failed to get config: %v", err)
+ }
+
+ return config, nil
+}
+
+// PutConfig ..
+func (p *ZooKeeperProvider) PutConfig(ctx context.Context, key string, config *Config) error {
+ conn := p.pool.Get()
+ defer p.pool.Put(conn)
+ data, err := json.Marshal(config)
+ if err != nil {
+ return fmt.Errorf("failed to marshal config: %v", err)
+ }
+
+ lockKey := key + "_lock"
+ if err := p.ensurePath(conn, lockKey); err != nil {
+ return fmt.Errorf("failed to ensure lock path %s: %v", lockKey, err)
+ }
+
+ lock := zk.NewLock(conn, lockKey, zk.WorldACL(zk.PermAll))
+ if err = lock.Lock(); err != nil {
+ return fmt.Errorf("failed to acquire zookeeper lock: %v", err)
+ }
+ defer lock.Unlock()
+ operation := func() error {
+ exists, _, err := conn.Exists(key)
+ if err != nil {
+ return err
+ }
+ if !exists {
+ _, err = conn.Create(key, data, 0, zk.WorldACL(zk.PermAll))
+ } else {
+ _, err = conn.Set(key, data, -1)
+ }
+ return err
+ }
+ if err = helper.WithRetry(ctx, operation); err != nil {
+ return fmt.Errorf("failed to put config: %v", err)
+ }
+
+ p.outLog(helper.OutputLogTypeInfo, fmt.Sprintf("[ZooKeeperProvider] Config written with lock, key: %v", key))
+ return nil
+}
+
+// WatchConfig ..
+func (p *ZooKeeperProvider) WatchConfig(ctx context.Context, key string, callback func(*Config)) error {
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ conn := p.pool.Get()
+ data, _, eventChan, err := conn.GetW(key)
+ if err != nil {
+ p.outLog(helper.OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperProvider] ZooKeeper watch error, key: %v", key))
+ p.pool.Put(conn)
+ time.Sleep(time.Second)
+ continue
+ }
+ if len(data) > 0 {
+ var config Config
+ if err = json.Unmarshal(data, &config); err == nil {
+ callback(&config)
+ }
+ }
+ p.pool.Put(conn)
+
+ select {
+ case ev := <-eventChan:
+ if ev.Type == zk.EventNodeDataChanged {
+ data, _, err := conn.Get(key)
+ if err == nil {
+ var config Config
+ if err := json.Unmarshal(data, &config); err == nil {
+ callback(&config)
+ }
+ }
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+ }
+ }()
+ return nil
+}
+
+// HealthCheck ..
+func (p *ZooKeeperProvider) HealthCheck(ctx context.Context) HealthStatus {
+ conn := p.pool.Get()
+ defer p.pool.Put(conn)
+ metrics := make(map[string]interface{})
+ start := time.Now()
+
+ // Check the connection status
+ _, _, err := conn.Get("/zookeeper")
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("zookeeper health check failed: %v", err)}
+ }
+ metrics["latency_ms"] = time.Since(start).Milliseconds()
+
+ // Check the writing capability
+ testKey := "/health/test"
+ if err = p.ensurePath(conn, testKey); err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("zookeeper create path failed: %v", testKey)}
+ }
+
+ _, err = conn.Create(testKey, []byte("test"), zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
+ if err != nil && err != zk.ErrNodeExists {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("zookeeper write check failed: %v", err)}
+ }
+ if err == nil {
+ err = conn.Delete(testKey, -1)
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("zookeeper delete check failed: %v", err)}
+ }
+ }
+
+ // Check the status of the cluster
+ children, _, err := conn.Children("/zookeeper")
+ if err != nil {
+ return HealthStatus{Metrics: metrics, Err: fmt.Errorf("zookeeper cluster check failed: %v", err)}
+ }
+ metrics["members"] = len(children)
+ return HealthStatus{Metrics: metrics}
+}
+
+// Close ..
+func (p *ZooKeeperProvider) Close() error {
+ p.pool.Close()
+ return nil
+}
diff --git a/foundation/clientpool/consul_pool.go b/foundation/clientpool/consul_pool.go
new file mode 100644
index 0000000..c36c1ec
--- /dev/null
+++ b/foundation/clientpool/consul_pool.go
@@ -0,0 +1,73 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package clientpool
+
+import (
+ "fmt"
+
+ "github.com/hashicorp/consul/api"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+)
+
+// ConsulPool manage the Consul connection pool
+type ConsulPool struct {
+ clients chan *api.Client
+ config *extraconfig.ConsulExtraConfig
+}
+
+// NewConsulPool ..
+func NewConsulPool(poolSize int, serverAddrs []string, config *extraconfig.ConsulExtraConfig) (*ConsulPool, error) {
+ pSize := poolSize
+ if poolSize <= len(serverAddrs) {
+ pSize = len(serverAddrs)
+ }
+
+ clients := make(chan *api.Client, pSize)
+
+ for j := 0; j < pSize; j++ {
+ cfg := api.DefaultConfig()
+ err := config.MergeTo(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to set consul config: %v", err)
+ }
+
+ if j < len(serverAddrs) {
+ cfg.Address = serverAddrs[j]
+ } else if len(serverAddrs) > 0 {
+ cfg.Address = serverAddrs[0]
+ }
+
+ cli, err := api.NewClient(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create consul client: %v", err)
+ }
+ clients <- cli
+ }
+
+ return &ConsulPool{clients: clients}, nil
+}
+
+// Get ..
+func (p *ConsulPool) Get() *api.Client {
+ return <-p.clients
+}
+
+// Put ..
+func (p *ConsulPool) Put(cli *api.Client) {
+ select {
+ case p.clients <- cli:
+ default:
+ // @Pass
+ }
+}
+
+// Close ..
+func (p *ConsulPool) Close() {
+ for len(p.clients) > 0 {
+ <-p.clients
+ }
+}
diff --git a/foundation/clientpool/etcd_pool.go b/foundation/clientpool/etcd_pool.go
new file mode 100644
index 0000000..63ca96a
--- /dev/null
+++ b/foundation/clientpool/etcd_pool.go
@@ -0,0 +1,62 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package clientpool
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "go.etcd.io/etcd/client/v3"
+)
+
+// EtcdPool manage the Etcd connection pool
+type EtcdPool struct {
+ clients chan *clientv3.Client
+ config *extraconfig.EtcdExtraConfig
+}
+
+// NewEtcdPool ..
+func NewEtcdPool(poolSize int, serverAddrs []string, config *extraconfig.EtcdExtraConfig) (*EtcdPool, error) {
+ clients := make(chan *clientv3.Client, poolSize)
+ for i := 0; i < poolSize; i++ {
+ cfg := clientv3.Config{Endpoints: serverAddrs, DialTimeout: 5 * time.Second}
+ err := config.MergeTo(&cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ cli, err := clientv3.New(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create etcd client: %v", err)
+ }
+ clients <- cli
+ }
+ return &EtcdPool{clients: clients, config: config}, nil
+}
+
+// Get ..
+func (p *EtcdPool) Get() *clientv3.Client {
+ return <-p.clients
+}
+
+// Put ..
+func (p *EtcdPool) Put(cli *clientv3.Client) {
+ select {
+ case p.clients <- cli:
+ default:
+ cli.Close()
+ }
+}
+
+// Close ..
+func (p *EtcdPool) Close() {
+ for len(p.clients) > 0 {
+ cli := <-p.clients
+ cli.Close()
+ }
+}
diff --git a/foundation/clientpool/nacos_pool.go b/foundation/clientpool/nacos_pool.go
new file mode 100644
index 0000000..ad86450
--- /dev/null
+++ b/foundation/clientpool/nacos_pool.go
@@ -0,0 +1,154 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package clientpool
+
+import (
+ "fmt"
+
+ "github.com/nacos-group/nacos-sdk-go/v2/clients"
+ "github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
+ "github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client"
+ "github.com/nacos-group/nacos-sdk-go/v2/common/constant"
+ "github.com/nacos-group/nacos-sdk-go/v2/vo"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+// NacosConfigPool manage the Nacos connection pool
+type NacosConfigPool struct {
+ clientChans chan config_client.IConfigClient
+ config *extraconfig.NacosExtraConfig
+}
+
+// NewNacosConfigPool ..
+func NewNacosConfigPool(poolSize int, serverAddrs []string, config *extraconfig.NacosExtraConfig) (*NacosConfigPool, error) {
+ clientChans := make(chan config_client.IConfigClient, poolSize)
+
+ var sCfg = make([]constant.ServerConfig, len(serverAddrs))
+ for index, addr := range serverAddrs {
+ addrs := addr
+ host, port, err := helper.SplitHostPort(addrs)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create nacos client: %v", err)
+ }
+
+ sCfg[index] = constant.ServerConfig{
+ IpAddr: host,
+ Port: uint64(port),
+ }
+ }
+
+ cCfg := *constant.NewClientConfig(
+ constant.WithTimeoutMs(5000),
+ constant.WithNotLoadCacheAtStart(true),
+ )
+
+ err := config.MergeTo(&cCfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to set nacos config: %v", err)
+ }
+
+ for i := 0; i < poolSize; i++ {
+ client, err := clients.CreateConfigClient(map[string]interface{}{
+ constant.KEY_SERVER_CONFIGS: sCfg,
+ constant.KEY_CLIENT_CONFIG: cCfg,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create nacos client: %v", err)
+ }
+ clientChans <- client
+ }
+ return &NacosConfigPool{clientChans: clientChans, config: config}, nil
+}
+
+// Get ..
+func (p *NacosConfigPool) Get() config_client.IConfigClient {
+ return <-p.clientChans
+}
+
+// Put ..
+func (p *NacosConfigPool) Put(client config_client.IConfigClient) {
+ select {
+ case p.clientChans <- client:
+ default:
+ // @Pass
+ }
+}
+
+// Close ..
+func (p *NacosConfigPool) Close() {
+ for len(p.clientChans) > 0 {
+ <-p.clientChans
+ }
+}
+
+/////////////////////////////////////////////////////////////////
+
+// NacosNamingPool manage the Nacos connection pool
+type NacosNamingPool struct {
+ clientChans chan naming_client.INamingClient
+ config *extraconfig.NacosExtraConfig
+}
+
+// NewNacosNamingPool ..
+func NewNacosNamingPool(poolSize int, serverAddrs []string, config *extraconfig.NacosExtraConfig) (*NacosNamingPool, error) {
+ clientChans := make(chan naming_client.INamingClient, poolSize)
+
+ var sCfg = make([]constant.ServerConfig, len(serverAddrs))
+ for index, addr := range serverAddrs {
+ addrs := addr
+ host, port, err := helper.SplitHostPort(addrs)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create nacos client: %v", err)
+ }
+
+ sCfg[index] = *constant.NewServerConfig(host, uint64(port))
+ }
+
+ cCfg := *constant.NewClientConfig(
+ constant.WithTimeoutMs(5000),
+ constant.WithNotLoadCacheAtStart(true),
+ )
+
+ err := config.MergeTo(&cCfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to set nacos config: %v", err)
+ }
+
+ for i := 0; i < poolSize; i++ {
+ client, err := clients.NewNamingClient(vo.NacosClientParam{
+ ClientConfig: &cCfg,
+ ServerConfigs: sCfg,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create nacos client: %v", err)
+ }
+ clientChans <- client
+ }
+ return &NacosNamingPool{clientChans: clientChans, config: config}, nil
+}
+
+// Get ..
+func (p *NacosNamingPool) Get() naming_client.INamingClient {
+ return <-p.clientChans
+}
+
+// Put ..
+func (p *NacosNamingPool) Put(client naming_client.INamingClient) {
+ select {
+ case p.clientChans <- client:
+ default:
+ // @Pass
+ }
+}
+
+// Close ..
+func (p *NacosNamingPool) Close() {
+ for len(p.clientChans) > 0 {
+ <-p.clientChans
+ }
+}
diff --git a/foundation/clientpool/zookeeper_pool.go b/foundation/clientpool/zookeeper_pool.go
new file mode 100644
index 0000000..16c5201
--- /dev/null
+++ b/foundation/clientpool/zookeeper_pool.go
@@ -0,0 +1,87 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package clientpool
+
+import (
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/go-zookeeper/zk"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+)
+
+// ZooKeeperPool Manage the ZooKeeper connection pool
+type ZooKeeperPool struct {
+ conns chan *zk.Conn
+ config *extraconfig.ZooKeeperExtraConfig
+}
+
+// NewZooKeeperPool ..
+func NewZooKeeperPool(poolSize int, serverAddrs []string, config *extraconfig.ZooKeeperExtraConfig) (*ZooKeeperPool, error) {
+ conns := make(chan *zk.Conn, poolSize)
+ zl := &extraconfig.Zlogger{OutLogCallback: func(format string, s ...interface{}) {
+ olc := config.GetZlogger()
+ if olc != nil {
+ olc.Printf("[go-zookeeper/zk logger] "+format, s...)
+ } else {
+ log.Printf("[go-zookeeper/zk logger] "+format, s...)
+ }
+ }}
+
+ for i := 0; i < poolSize; i++ {
+ var daler zk.Dialer
+ if config.GetTLSConfig() != nil {
+ daler = config.CreateTlsDialer()
+ }
+
+ var conn *zk.Conn
+ //var events <-chan zk.Event
+ var err error
+
+ if daler != nil {
+ conn, _, err = zk.ConnectWithDialer(serverAddrs, time.Second*5, daler)
+ } else {
+ conn, _, err = zk.Connect(serverAddrs, time.Second*5)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("failed to create zookeeper client: %v", err)
+ }
+
+ conn.SetLogger(zl)
+
+ err = config.MergeTo(conn)
+ if err != nil {
+ return nil, err
+ }
+
+ conns <- conn
+ }
+ return &ZooKeeperPool{conns: conns, config: config}, nil
+}
+
+// Get ..
+func (p *ZooKeeperPool) Get() *zk.Conn {
+ return <-p.conns
+}
+
+// Put ..
+func (p *ZooKeeperPool) Put(conn *zk.Conn) {
+ select {
+ case p.conns <- conn:
+ default:
+ conn.Close()
+ }
+}
+
+// Close ..
+func (p *ZooKeeperPool) Close() {
+ for len(p.conns) > 0 {
+ conn := <-p.conns
+ conn.Close()
+ }
+}
diff --git a/foundation/common/tls.go b/foundation/common/tls.go
new file mode 100644
index 0000000..51a995c
--- /dev/null
+++ b/foundation/common/tls.go
@@ -0,0 +1,52 @@
+package common
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "os"
+)
+
+// TLSConfig .
+type TLSConfig struct {
+ Address string // Address
+ CertFile string // Client certificate file
+ KeyFile string // Client private key file
+ CAFile string // CA certificate file
+ ServerName string // Server name (optional)
+}
+
+// CreateTLSConfig create TLS configuration
+func CreateTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) {
+ if tlsConfig == nil {
+ return nil, nil
+ }
+
+ // Load the client certificate and private key
+ cert, err := tls.LoadX509KeyPair(tlsConfig.CertFile, tlsConfig.KeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("the client certificate cannot be loaded: %v", err)
+ }
+
+ // Load the CA certificate
+ caCert, err := os.ReadFile(tlsConfig.CAFile)
+ if err != nil {
+ return nil, fmt.Errorf("the CA certificate cannot be read: %v", err)
+ }
+ caCertPool := x509.NewCertPool()
+ if !caCertPool.AppendCertsFromPEM(caCert) {
+ return nil, fmt.Errorf("the CA certificate cannot be parsed")
+ }
+
+ tlsConf := &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ RootCAs: caCertPool,
+ InsecureSkipVerify: false,
+ }
+
+ if tlsConfig.ServerName != "" {
+ tlsConf.ServerName = tlsConfig.ServerName
+ }
+
+ return tlsConf, nil
+}
diff --git a/foundation/extraconfig/consul_config.go b/foundation/extraconfig/consul_config.go
new file mode 100644
index 0000000..7c226d1
--- /dev/null
+++ b/foundation/extraconfig/consul_config.go
@@ -0,0 +1,91 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package extraconfig
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/hashicorp/consul/api"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/helper"
+)
+
+// ConsulExtraConfig .
+type ConsulExtraConfig struct {
+ Username string
+ Password string
+ Scheme string
+ PathPrefix string
+ Datacenter string
+ Transport *http.Transport
+ HttpClient *http.Client
+ WaitTime time.Duration
+ Token string
+ TokenFile string
+ Namespace string
+ Partition string
+
+ tlsConfig *common.TLSConfig
+}
+
+// SetTLSConfig .
+func (ec *ConsulExtraConfig) SetTLSConfig(tls *common.TLSConfig) {
+ ec.tlsConfig = tls
+}
+
+// MergeTo .
+func (ec *ConsulExtraConfig) MergeTo(destConfig *api.Config) error {
+ if ec.Username != "" || ec.Password != "" {
+ destConfig.HttpAuth = &api.HttpBasicAuth{
+ Username: ec.Username,
+ Password: ec.Password,
+ }
+ }
+
+ if ec.tlsConfig != nil {
+ destConfig.TLSConfig = api.TLSConfig{
+ Address: ec.tlsConfig.Address,
+ CAFile: ec.tlsConfig.CAFile,
+ KeyFile: ec.tlsConfig.KeyFile,
+ CertFile: ec.tlsConfig.CertFile,
+ }
+ }
+
+ if helper.IsDurationSet(ec.WaitTime) {
+ destConfig.WaitTime = ec.WaitTime
+ }
+ if ec.Scheme != "" {
+ destConfig.Scheme = ec.Scheme
+ }
+ if ec.PathPrefix != "" {
+ destConfig.PathPrefix = ec.PathPrefix
+ }
+ if ec.Datacenter != "" {
+ destConfig.Datacenter = ec.Datacenter
+ }
+ if ec.Transport != nil {
+ destConfig.Transport = ec.Transport
+ }
+ if ec.HttpClient != nil {
+ destConfig.HttpClient = ec.HttpClient
+ }
+ if ec.Token != "" {
+ destConfig.Token = ec.Token
+ }
+ if ec.TokenFile != "" {
+ destConfig.TokenFile = ec.TokenFile
+ }
+ if ec.Namespace != "" {
+ destConfig.Namespace = ec.Namespace
+ }
+ if ec.Partition != "" {
+ destConfig.Partition = ec.Partition
+ }
+
+ return nil
+}
diff --git a/foundation/extraconfig/etcd_config.go b/foundation/extraconfig/etcd_config.go
new file mode 100644
index 0000000..b32ffed
--- /dev/null
+++ b/foundation/extraconfig/etcd_config.go
@@ -0,0 +1,113 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package extraconfig
+
+import (
+ "context"
+ "time"
+
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/helper"
+ clientv3 "go.etcd.io/etcd/client/v3"
+ "go.uber.org/zap"
+ "google.golang.org/grpc"
+)
+
+// EtcdExtraConfig .
+type EtcdExtraConfig struct {
+ Username string
+ Password string
+ AutoSyncInterval time.Duration
+ DialTimeout time.Duration
+ DialKeepAliveTime time.Duration
+ DialKeepAliveTimeout time.Duration
+ MaxCallSendMsgSize int
+ MaxCallRecvMsgSize int
+ RejectOldCluster bool
+ DialOptions []grpc.DialOption
+ Context context.Context
+ Logger *zap.Logger
+ LogConfig *zap.Config
+ PermitWithoutStream bool
+ MaxUnaryRetries uint
+ BackoffWaitBetween time.Duration
+ BackoffJitterFraction float64
+ tlsConfig *common.TLSConfig
+}
+
+// SetTLSConfig .
+func (ec *EtcdExtraConfig) SetTLSConfig(tls *common.TLSConfig) {
+ ec.tlsConfig = tls
+}
+
+// MergeTo .
+func (ec *EtcdExtraConfig) MergeTo(destConfig *clientv3.Config) error {
+ if ec.Username != "" {
+ destConfig.Username = ec.Username
+ }
+
+ if ec.Password != "" {
+ destConfig.Password = ec.Password
+ }
+
+ if helper.IsDurationSet(ec.AutoSyncInterval) {
+ destConfig.AutoSyncInterval = ec.AutoSyncInterval
+ }
+ if helper.IsDurationSet(ec.DialTimeout) {
+ destConfig.DialTimeout = ec.DialTimeout
+ }
+ if helper.IsDurationSet(ec.DialKeepAliveTime) {
+ destConfig.DialKeepAliveTime = ec.DialKeepAliveTime
+ }
+ if helper.IsDurationSet(ec.DialKeepAliveTimeout) {
+ destConfig.DialKeepAliveTimeout = ec.DialKeepAliveTimeout
+ }
+ if ec.MaxCallSendMsgSize > 0 {
+ destConfig.MaxCallSendMsgSize = ec.MaxCallSendMsgSize
+ }
+ if ec.MaxCallRecvMsgSize > 0 {
+ destConfig.MaxCallRecvMsgSize = ec.MaxCallRecvMsgSize
+ }
+
+ if ec.RejectOldCluster {
+ destConfig.RejectOldCluster = ec.RejectOldCluster
+ }
+ if len(ec.DialOptions) > 0 {
+ destConfig.DialOptions = ec.DialOptions
+ }
+ if ec.Context != nil {
+ destConfig.Context = ec.Context
+ }
+ if ec.Logger != nil {
+ destConfig.Logger = ec.Logger
+ }
+ if ec.LogConfig != nil {
+ destConfig.LogConfig = ec.LogConfig
+ }
+ if ec.PermitWithoutStream {
+ destConfig.PermitWithoutStream = ec.PermitWithoutStream
+ }
+ if ec.MaxUnaryRetries > 0 {
+ destConfig.MaxUnaryRetries = ec.MaxUnaryRetries
+ }
+ if helper.IsDurationSet(ec.BackoffWaitBetween) {
+ destConfig.BackoffWaitBetween = ec.BackoffWaitBetween
+ }
+ if ec.BackoffJitterFraction > 0.0 {
+ destConfig.BackoffJitterFraction = ec.BackoffJitterFraction
+ }
+
+ if ec.tlsConfig != nil {
+ var err error
+ destConfig.TLS, err = common.CreateTLSConfig(ec.tlsConfig)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/foundation/extraconfig/nacos_config.go b/foundation/extraconfig/nacos_config.go
new file mode 100644
index 0000000..2a2baf8
--- /dev/null
+++ b/foundation/extraconfig/nacos_config.go
@@ -0,0 +1,144 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package extraconfig
+
+import (
+ "github.com/nacos-group/nacos-sdk-go/v2/common/constant"
+ "github.com/wenlng/go-service-link/foundation/common"
+)
+
+// NacosExtraConfig .
+type NacosExtraConfig struct {
+ Username string
+ Password string
+ TimeoutMs uint64
+ BeatInterval int64
+ NamespaceId string
+ AppName string
+ AppKey string
+ Endpoint string
+ RegionId string
+ AccessKey string
+ SecretKey string
+ OpenKMS bool
+ CacheDir string
+ DisableUseSnapShot bool
+ UpdateThreadNum int
+ NotLoadCacheAtStart bool
+ UpdateCacheWhenEmpty bool
+ LogDir string
+ LogLevel string
+ ContextPath string
+ AppendToStdout bool
+ AsyncUpdateService bool
+ EndpointContextPath string
+ EndpointQueryParams string
+ ClusterName string
+
+ tlsConfig *common.TLSConfig
+}
+
+// SetTLSConfig ..
+func (ec *NacosExtraConfig) SetTLSConfig(tls *common.TLSConfig) {
+ ec.tlsConfig = tls
+}
+
+// MergeTo .
+func (ec *NacosExtraConfig) MergeTo(destConfig *constant.ClientConfig) error {
+ if ec.Username != "" {
+ destConfig.Username = ec.Username
+ }
+
+ if ec.Password != "" {
+ destConfig.Password = ec.Password
+ }
+
+ if ec.TimeoutMs > 0 {
+ destConfig.TimeoutMs = ec.TimeoutMs
+ }
+ if ec.BeatInterval > 0 {
+ destConfig.BeatInterval = ec.BeatInterval
+ }
+ if ec.NamespaceId != "" {
+ destConfig.NamespaceId = ec.NamespaceId
+ }
+ if ec.AppName != "" {
+ destConfig.AppName = ec.AppName
+ }
+ if ec.AppKey != "" {
+ destConfig.AppKey = ec.AppKey
+ }
+ if ec.AppKey != "" {
+ destConfig.AppKey = ec.AppKey
+ }
+ if ec.Endpoint != "" {
+ destConfig.Endpoint = ec.Endpoint
+ }
+ if ec.RegionId != "" {
+ destConfig.RegionId = ec.RegionId
+ }
+ if ec.AccessKey != "" {
+ destConfig.AccessKey = ec.AccessKey
+ }
+ if ec.SecretKey != "" {
+ destConfig.SecretKey = ec.SecretKey
+ }
+ if ec.OpenKMS {
+ destConfig.OpenKMS = ec.OpenKMS
+ }
+ if ec.CacheDir != "" {
+ destConfig.CacheDir = ec.CacheDir
+ }
+ if ec.DisableUseSnapShot {
+ destConfig.DisableUseSnapShot = ec.DisableUseSnapShot
+ }
+ if ec.UpdateThreadNum > 0 {
+ destConfig.UpdateThreadNum = ec.UpdateThreadNum
+ }
+ if ec.NotLoadCacheAtStart {
+ destConfig.NotLoadCacheAtStart = ec.NotLoadCacheAtStart
+ }
+ if ec.UpdateCacheWhenEmpty {
+ destConfig.UpdateCacheWhenEmpty = ec.UpdateCacheWhenEmpty
+ }
+ if ec.LogDir != "" {
+ destConfig.LogDir = ec.LogDir
+ }
+ if ec.LogLevel != "" {
+ destConfig.LogLevel = ec.LogLevel
+ }
+ if ec.ContextPath != "" {
+ destConfig.ContextPath = ec.ContextPath
+ }
+ if ec.AppendToStdout {
+ destConfig.AppendToStdout = ec.AppendToStdout
+ }
+ if ec.AsyncUpdateService {
+ destConfig.AsyncUpdateService = ec.AsyncUpdateService
+ }
+ if ec.EndpointContextPath != "" {
+ destConfig.EndpointContextPath = ec.EndpointContextPath
+ }
+ if ec.EndpointQueryParams != "" {
+ destConfig.EndpointQueryParams = ec.EndpointQueryParams
+ }
+ if ec.ClusterName != "" {
+ destConfig.ClusterName = ec.ClusterName
+ }
+
+ if ec.tlsConfig != nil {
+ destConfig.TLSCfg = constant.TLSConfig{
+ Enable: true,
+ CaFile: ec.tlsConfig.CAFile,
+ CertFile: ec.tlsConfig.CertFile,
+ KeyFile: ec.tlsConfig.KeyFile,
+ ServerNameOverride: ec.tlsConfig.ServerName,
+ }
+ }
+
+ return nil
+}
diff --git a/foundation/extraconfig/zookeeper_config.go b/foundation/extraconfig/zookeeper_config.go
new file mode 100644
index 0000000..502cdf7
--- /dev/null
+++ b/foundation/extraconfig/zookeeper_config.go
@@ -0,0 +1,89 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package extraconfig
+
+import (
+ "crypto/tls"
+ "fmt"
+ "log"
+ "net"
+ "time"
+
+ "github.com/go-zookeeper/zk"
+ "github.com/wenlng/go-service-link/foundation/common"
+)
+
+type Zlogger struct {
+ OutLogCallback func(format string, s ...interface{})
+}
+
+func (l *Zlogger) Printf(format string, s ...interface{}) {
+ if l.OutLogCallback != nil {
+ l.OutLogCallback(format, s...)
+ } else {
+ log.Printf("[go-zookeeper/zk logger] "+format, s...)
+ }
+}
+
+// ZooKeeperExtraConfig .
+type ZooKeeperExtraConfig struct {
+ Username string
+ Password string
+ Timeout time.Duration
+ MaxBufferSize int
+ MaxConnBufferSize int
+ tlsConfig *common.TLSConfig
+ zlogger *Zlogger
+}
+
+// SetZlogger .
+func (ec *ZooKeeperExtraConfig) SetZlogger(l *Zlogger) {
+ ec.zlogger = l
+}
+
+// GetZlogger .
+func (ec *ZooKeeperExtraConfig) GetZlogger() *Zlogger {
+ return ec.zlogger
+}
+
+// SetTLSConfig .
+func (ec *ZooKeeperExtraConfig) SetTLSConfig(tls *common.TLSConfig) {
+ ec.tlsConfig = tls
+}
+
+// GetTLSConfig .
+func (ec *ZooKeeperExtraConfig) GetTLSConfig() *common.TLSConfig {
+ return ec.tlsConfig
+}
+
+// MergeTo .
+func (ec *ZooKeeperExtraConfig) MergeTo(conn *zk.Conn) error {
+ if ec.Username != "" || ec.Password != "" {
+ auth := fmt.Sprintf("digest:%s:%s", ec.Username, ec.Password)
+ err := conn.AddAuth("digest", []byte(auth))
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// CreateTlsDialer .
+func (ec *ZooKeeperExtraConfig) CreateTlsDialer() zk.Dialer {
+ return func(network, address string, timeout time.Duration) (net.Conn, error) {
+ if ec.tlsConfig == nil {
+ return net.DialTimeout(network, address, timeout)
+ }
+ tlsConf, err := common.CreateTLSConfig(ec.tlsConfig)
+ if err != nil {
+ return nil, err
+ }
+ dialer := &net.Dialer{Timeout: timeout}
+ return tls.DialWithDialer(dialer, network, address, tlsConf)
+ }
+}
diff --git a/foundation/helper/helper.go b/foundation/helper/helper.go
new file mode 100644
index 0000000..cd9124f
--- /dev/null
+++ b/foundation/helper/helper.go
@@ -0,0 +1,139 @@
+package helper
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/cenkalti/backoff/v4"
+)
+
+// GetHostname .
+func GetHostname() string {
+ hostname, err := os.Hostname()
+ if err != nil {
+ return "-"
+ }
+ return hostname
+}
+
+// SplitHostPort .
+func SplitHostPort(addr string) (string, int, error) {
+ parts := strings.Split(addr, ":")
+ if len(parts) != 2 {
+ return "", 0, fmt.Errorf("invalid address format: %s", addr)
+ }
+ port, err := strconv.Atoi(parts[1])
+ if err != nil {
+ return "", 0, fmt.Errorf("invalid port: %s", parts[1])
+ }
+ return parts[0], port, nil
+}
+
+// IsDurationSet .
+func IsDurationSet(d time.Duration) bool {
+ return d != 0
+}
+
+// WithRetry index backoff and retry
+func WithRetry(ctx context.Context, operation func() error) error {
+ return WithRetryWithBc(ctx, operation, &backoff.ExponentialBackOff{
+ MaxElapsedTime: 30 * time.Second,
+ })
+}
+
+// WithRetryWithBc index backoff and retry
+func WithRetryWithBc(ctx context.Context, operation func() error, bc *backoff.ExponentialBackOff) error {
+ b := backoff.NewExponentialBackOff()
+
+ if bc != nil {
+ if IsDurationSet(bc.MaxElapsedTime) {
+ b.MaxElapsedTime = bc.MaxElapsedTime
+ } else {
+ b.MaxElapsedTime = 30 * time.Second
+ }
+ if IsDurationSet(bc.InitialInterval) {
+ b.InitialInterval = bc.InitialInterval
+ }
+ if IsDurationSet(bc.MaxInterval) {
+ b.MaxInterval = bc.MaxInterval
+ }
+ if bc.RandomizationFactor > 0.0 {
+ b.RandomizationFactor = bc.RandomizationFactor
+ }
+ if bc.Multiplier > 0.0 {
+ b.Multiplier = bc.Multiplier
+ }
+ } else {
+ b.MaxElapsedTime = 30 * time.Second
+ }
+
+ return backoff.Retry(func() error {
+ select {
+ case <-ctx.Done():
+ return backoff.Permanent(ctx.Err())
+ default:
+ return operation()
+ }
+ }, backoff.WithContext(b, ctx))
+}
+
+// DoRetry perform the operation using the retry logic
+func DoRetry(operation func() error, maxRetries int, baseRetryDelay time.Duration) error {
+ var err error
+ for attempt := 1; attempt <= maxRetries; attempt++ {
+ err = operation()
+ if err == nil {
+ return nil
+ }
+
+ // Calculate the delay with random jitter
+ b := 1 << uint(attempt-1)
+ delay := time.Duration(float64(baseRetryDelay) * float64(b))
+ jitter := time.Duration(rand.Intn(100)) * time.Millisecond
+ time.Sleep(delay + jitter)
+
+ // If the client fails, try to reconnect
+ if attempt < maxRetries {
+ err = operation()
+ if err == nil {
+ return nil
+ }
+ }
+ }
+ return nil
+}
+
+// EnsureHTTP ...
+func EnsureHTTP(url string) string {
+ if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
+ return "http://" + url
+ }
+ return url
+}
+
+// IsOnlyEmpty ..
+func IsOnlyEmpty(v interface{}) bool {
+ if v == nil {
+ return true
+ }
+
+ switch v := v.(type) {
+ case string:
+ return v == ""
+ case int:
+ return v == 0
+ case bool:
+ return !v
+ case []interface{}:
+ return len(v) == 0
+ case map[interface{}]interface{}:
+ return len(v) == 0
+ default:
+ return false
+ }
+}
diff --git a/foundation/helper/log_output.go b/foundation/helper/log_output.go
new file mode 100644
index 0000000..8c81ac3
--- /dev/null
+++ b/foundation/helper/log_output.go
@@ -0,0 +1,14 @@
+package helper
+
+// OutputLogType .
+type OutputLogType string
+
+const (
+ OutputLogTypeWarn OutputLogType = "warn"
+ OutputLogTypeInfo = "info"
+ OutputLogTypeError = "error"
+ OutputLogTypeDebug = "debug"
+)
+
+// OutputLogCallback ..
+type OutputLogCallback = func(logType OutputLogType, message string)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1f81855
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,93 @@
+module github.com/wenlng/go-service-link
+
+go 1.23.0
+
+require (
+ github.com/cenkalti/backoff/v4 v4.3.0
+ github.com/go-zookeeper/zk v1.0.3
+ github.com/google/uuid v1.6.0
+ github.com/hashicorp/consul/api v1.29.4
+ github.com/nacos-group/nacos-sdk-go/v2 v2.2.8
+ github.com/stretchr/testify v1.9.0
+ go.etcd.io/etcd/client/v3 v3.5.21
+ go.uber.org/zap v1.27.0
+ google.golang.org/grpc v1.67.1
+)
+
+require (
+ github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
+ github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
+ github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
+ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect
+ github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect
+ github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 // indirect
+ github.com/alibabacloud-go/darabonba-signature-util v0.0.7 // indirect
+ github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect
+ github.com/alibabacloud-go/debug v1.0.1 // indirect
+ github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
+ github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 // indirect
+ github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
+ github.com/alibabacloud-go/tea v1.2.2 // indirect
+ github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
+ github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
+ github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
+ github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect
+ github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect
+ github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
+ github.com/aliyun/credentials-go v1.4.3 // indirect
+ github.com/armon/go-metrics v0.4.1 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/buger/jsonparser v1.1.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/clbanning/mxj/v2 v2.5.5 // indirect
+ github.com/coreos/go-semver v0.3.1 // indirect
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/fatih/color v1.16.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/mock v1.6.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+ github.com/hashicorp/go-hclog v1.5.0 // indirect
+ github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-rootcerts v1.0.2 // indirect
+ github.com/hashicorp/golang-lru v0.5.4 // indirect
+ github.com/hashicorp/serf v0.10.1 // indirect
+ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
+ github.com/json-iterator/go v1.1.12 // 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.20 // indirect
+ github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_golang v1.12.2 // indirect
+ github.com/prometheus/client_model v0.2.0 // indirect
+ github.com/prometheus/common v0.32.1 // indirect
+ github.com/prometheus/procfs v0.7.3 // indirect
+ github.com/rogpeppe/go-internal v1.10.0 // indirect
+ github.com/tjfoc/gmsm v1.4.1 // indirect
+ go.etcd.io/etcd/api/v3 v3.5.21 // indirect
+ go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/crypto v0.36.0 // indirect
+ golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
+ golang.org/x/net v0.38.0 // indirect
+ golang.org/x/sync v0.12.0 // indirect
+ golang.org/x/sys v0.31.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
+ golang.org/x/time v0.6.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..4abf630
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,820 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
+github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
+github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
+github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
+github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
+github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
+github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
+github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
+github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
+github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
+github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
+github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
+github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
+github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
+github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
+github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
+github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
+github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
+github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
+github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
+github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
+github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 h1:vamGcYQFwXVqR6RWcrVTTqlIXZVsYjaA7pZbx+Xw6zw=
+github.com/alibabacloud-go/kms-20160120/v3 v3.2.3/go.mod h1:3rIyughsFDLie1ut9gQJXkWkMg/NfXBCk+OtXnPu3lw=
+github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
+github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
+github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
+github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
+github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
+github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
+github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
+github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
+github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
+github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
+github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
+github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
+github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
+github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
+github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
+github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
+github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
+github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU=
+github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg=
+github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 h1:QeUdR7JF7iNCvO/81EhxEr3wDwxk4YBoYZOq6E0AjHI=
+github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU=
+github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
+github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
+github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
+github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
+github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEpgeGttY=
+github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
+github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
+github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
+github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
+github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+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=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE=
+github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg=
+github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0=
+github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
+github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
+github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
+github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
+github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
+github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
+github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
+github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
+github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
+github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
+github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
+github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nacos-group/nacos-sdk-go/v2 v2.2.8 h1:lyt2ynw2+G4JywFUAM0qXCJ/nEA9ORsweiLkgwYISUo=
+github.com/nacos-group/nacos-sdk-go/v2 v2.2.8/go.mod h1:pIv1y5pMAjqFNU4jo9fmbuBjiZqpeGCddKkv0Y12ysY=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
+github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
+github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
+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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
+github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
+github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
+github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
+go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
+go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
+go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
+go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
+go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
+golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U=
+google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/servicediscovery/balancer/consistent_hash.go b/servicediscovery/balancer/consistent_hash.go
new file mode 100644
index 0000000..db165f4
--- /dev/null
+++ b/servicediscovery/balancer/consistent_hash.go
@@ -0,0 +1,66 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package balancer
+
+import (
+ "fmt"
+ "hash/crc32"
+ "sync"
+
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// ConsistentHashBalancer .
+type ConsistentHashBalancer struct {
+ hashRing []uint32
+ nodes map[uint32]instance.ServiceInstance
+ mu sync.RWMutex
+}
+
+// NewConsistentHashBalancer .
+func NewConsistentHashBalancer() LoadBalancer {
+ return &ConsistentHashBalancer{
+ nodes: make(map[uint32]instance.ServiceInstance),
+ }
+}
+
+// Select .
+func (b *ConsistentHashBalancer) Select(instances []instance.ServiceInstance, key string) (instance.ServiceInstance, error) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ if len(instances) == 0 {
+ return instance.ServiceInstance{}, fmt.Errorf("no instances available")
+ }
+
+ if len(b.hashRing) != len(instances)*10 {
+ b.hashRing = nil
+ b.nodes = make(map[uint32]instance.ServiceInstance)
+ for _, inst := range instances {
+ for i := 0; i < 10; i++ {
+ hash := crc32.ChecksumIEEE([]byte(fmt.Sprintf("%s:%d", inst.Host, i)))
+ b.hashRing = append(b.hashRing, hash)
+ b.nodes[hash] = inst
+ }
+ }
+ for i := 0; i < len(b.hashRing)-1; i++ {
+ for j := i + 1; j < len(b.hashRing); j++ {
+ if b.hashRing[i] > b.hashRing[j] {
+ b.hashRing[i], b.hashRing[j] = b.hashRing[j], b.hashRing[i]
+ }
+ }
+ }
+ }
+
+ hash := crc32.ChecksumIEEE([]byte(key))
+ for _, h := range b.hashRing {
+ if h >= hash {
+ return b.nodes[h], nil
+ }
+ }
+ return b.nodes[b.hashRing[0]], nil
+}
diff --git a/servicediscovery/balancer/load_balancer.go b/servicediscovery/balancer/load_balancer.go
new file mode 100644
index 0000000..54fdb57
--- /dev/null
+++ b/servicediscovery/balancer/load_balancer.go
@@ -0,0 +1,26 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package balancer
+
+import (
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// LoadBalancerType ..
+type LoadBalancerType string
+
+// LoadBalancerType .
+const (
+ LoadBalancerTypeRandom LoadBalancerType = "random"
+ LoadBalancerTypeRoundRobin = "round_robin"
+ LoadBalancerTypeConsistentHash = "consistent_hash"
+)
+
+// LoadBalancer load balance strategy
+type LoadBalancer interface {
+ Select(instances []instance.ServiceInstance, key string) (instance.ServiceInstance, error)
+}
diff --git a/servicediscovery/balancer/load_balancer_test.go b/servicediscovery/balancer/load_balancer_test.go
new file mode 100644
index 0000000..a542fa1
--- /dev/null
+++ b/servicediscovery/balancer/load_balancer_test.go
@@ -0,0 +1,68 @@
+package balancer
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLoadBalancers(t *testing.T) {
+ instances := []base.ServiceInstance{
+ {Host: "localhost:8081", Metadata: map[string]string{"http_port": "8081"}},
+ {Host: "localhost:8082", Metadata: map[string]string{"http_port": "8082"}},
+ {Host: "localhost:8083", Metadata: map[string]string{"http_port": "8083"}},
+ }
+
+ t.Run("RandomBalancer", func(t *testing.T) {
+ lb := NewRandomBalancer()
+ counts := make(map[string]int)
+ for i := 0; i < 1000; i++ {
+ inst, err := lb.Select(instances, "")
+ assert.NoError(t, err)
+ counts[inst.Host]++
+ }
+ for _, addr := range []string{"localhost:8081", "localhost:8082", "localhost:8083"} {
+ assert.Greater(t, counts[addr], 200, "Random balancer should distribute requests")
+ }
+ })
+
+ t.Run("RoundRobinBalancer", func(t *testing.T) {
+ lb := NewRoundRobinBalancer()
+ for i, expected := range []string{"localhost:8081", "localhost:8082", "localhost:8083", "localhost:8081"} {
+ inst, err := lb.Select(instances, "")
+ assert.NoError(t, err)
+ assert.Equal(t, expected, inst.Host, "Round robin balancer should select in order at iteration %d", i)
+ }
+ })
+
+ t.Run("ConsistentHashBalancer", func(t *testing.T) {
+ lb := NewConsistentHashBalancer()
+ key := "test-key"
+ var lastAddr string
+ for i := 0; i < 10; i++ {
+ inst, err := lb.Select(instances, key)
+ assert.NoError(t, err)
+ if i == 0 {
+ lastAddr = inst.Host
+ } else {
+ assert.Equal(t, lastAddr, inst.Host, "Consistent hash balancer should select same instance for same key")
+ }
+ }
+ inst, err := lb.Select(instances, "different-key")
+ assert.NoError(t, err)
+ assert.NotEqual(t, lastAddr, inst.Host, "Consistent hash balancer may select different instance for different key")
+ })
+
+ t.Run("EmptyInstances", func(t *testing.T) {
+ lbs := []LoadBalancer{
+ NewRandomBalancer(),
+ NewRoundRobinBalancer(),
+ NewConsistentHashBalancer(),
+ }
+ for _, lb := range lbs {
+ _, err := lb.Select([]base.ServiceInstance{}, "")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "no instances available")
+ }
+ })
+}
diff --git a/servicediscovery/balancer/random.go b/servicediscovery/balancer/random.go
new file mode 100644
index 0000000..b1b885b
--- /dev/null
+++ b/servicediscovery/balancer/random.go
@@ -0,0 +1,30 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package balancer
+
+import (
+ "fmt"
+ "math/rand"
+
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// RandomBalancer implements random load balancing
+type RandomBalancer struct{}
+
+// NewRandomBalancer ..
+func NewRandomBalancer() LoadBalancer {
+ return &RandomBalancer{}
+}
+
+// Select .
+func (b *RandomBalancer) Select(instances []instance.ServiceInstance, key string) (instance.ServiceInstance, error) {
+ if len(instances) == 0 {
+ return instance.ServiceInstance{}, fmt.Errorf("no instances available")
+ }
+ return instances[rand.Intn(len(instances))], nil
+}
diff --git a/servicediscovery/balancer/round_robin.go b/servicediscovery/balancer/round_robin.go
new file mode 100644
index 0000000..91afac6
--- /dev/null
+++ b/servicediscovery/balancer/round_robin.go
@@ -0,0 +1,33 @@
+package balancer
+
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+import (
+ "fmt"
+ "sync/atomic"
+
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// RoundRobinBalancer implements round-robin load balancing
+type RoundRobinBalancer struct {
+ counter uint64
+}
+
+// NewRoundRobinBalancer .
+func NewRoundRobinBalancer() LoadBalancer {
+ return &RoundRobinBalancer{}
+}
+
+// Select .
+func (b *RoundRobinBalancer) Select(instances []instance.ServiceInstance, key string) (instance.ServiceInstance, error) {
+ if len(instances) == 0 {
+ return instance.ServiceInstance{}, fmt.Errorf("no instances available")
+ }
+ index := atomic.AddUint64(&b.counter, 1) % uint64(len(instances))
+ return instances[index], nil
+}
diff --git a/servicediscovery/consul_discovery.go b/servicediscovery/consul_discovery.go
new file mode 100644
index 0000000..5365b93
--- /dev/null
+++ b/servicediscovery/consul_discovery.go
@@ -0,0 +1,340 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package servicediscovery
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/consul/api"
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// ConsulDiscovery .
+type ConsulDiscovery struct {
+ pool *clientpool.ConsulPool
+ outputLogCallback OutputLogCallback
+ config ConsulDiscoveryConfig
+
+ registeredServices map[string]registeredServiceInfo
+ mutex sync.RWMutex
+}
+
+// ConsulDiscoveryConfig .
+type ConsulDiscoveryConfig struct {
+ extraconfig.ConsulExtraConfig
+
+ address []string
+ poolSize int
+ ttl time.Duration
+ keepAlive time.Duration
+ maxRetries int
+ baseRetryDelay time.Duration
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+}
+
+// NewConsulDiscovery .
+func NewConsulDiscovery(config ConsulDiscoveryConfig) (*ConsulDiscovery, error) {
+ if config.poolSize <= 0 {
+ config.poolSize = 5
+ }
+
+ if config.maxRetries <= 0 {
+ config.maxRetries = 3
+ }
+
+ if !helper.IsDurationSet(config.baseRetryDelay) {
+ config.baseRetryDelay = 500 * time.Millisecond
+ }
+
+ config.ConsulExtraConfig.Username = config.username
+ config.ConsulExtraConfig.Password = config.password
+
+ pool, err := clientpool.NewConsulPool(config.poolSize, config.address, &config.ConsulExtraConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ConsulDiscovery{
+ config: config,
+ pool: pool,
+ registeredServices: make(map[string]registeredServiceInfo),
+ }, nil
+}
+
+// SetOutputLogCallback Set the log out hook function
+func (d *ConsulDiscovery) SetOutputLogCallback(outputLogCallback OutputLogCallback) {
+ d.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (d *ConsulDiscovery) outLog(logType OutputLogType, message string) {
+ if d.outputLogCallback != nil {
+ d.outputLogCallback(logType, message)
+ }
+}
+
+// register ..
+func (d *ConsulDiscovery) register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string, isReRegister bool) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ if instanceID == "" {
+ instanceID = uuid.New().String()
+ }
+
+ port, _ := strconv.Atoi(httpPort)
+ registration := &api.AgentServiceRegistration{
+ ID: instanceID,
+ Name: serviceName,
+ Address: host,
+ Port: port,
+ Meta: map[string]string{
+ "hostname": helper.GetHostname(),
+ "host": host,
+ "http_port": httpPort,
+ "grpc_port": grpcPort,
+ },
+ Check: &api.AgentServiceCheck{
+ TTL: (d.config.ttl + time.Second).String(),
+ DeregisterCriticalServiceAfter: (d.config.ttl * 2).String(),
+ },
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[ConsulDiscovery] The registration service is beginning...., service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+
+ if !isReRegister {
+ d.mutex.Lock()
+ d.registeredServices[instanceID] = registeredServiceInfo{
+ ServiceName: serviceName,
+ InstanceID: instanceID,
+ Host: host,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ }
+ d.mutex.Unlock()
+ go d.keepAliveLoop(ctx, instanceID)
+ }
+
+ operation := func() error {
+ return cli.Agent().ServiceRegister(registration)
+ }
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ return fmt.Errorf("failed to register instance: %v", err)
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[ConsulDiscovery] Registered instance, service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+ return nil
+}
+
+// Register .
+func (d *ConsulDiscovery) Register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string) error {
+ return d.register(ctx, serviceName, instanceID, host, httpPort, grpcPort, false)
+}
+
+// checkAndReRegisterServices Check and re-register the service
+func (d *ConsulDiscovery) checkAndReRegisterServices(ctx context.Context) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ d.mutex.RLock()
+ services := make(map[string]registeredServiceInfo)
+ for k, v := range d.registeredServices {
+ services[k] = v
+ }
+ d.mutex.RUnlock()
+
+ for instanceID, svcInfo := range services {
+ operation := func() error {
+ services, _, err := cli.Health().Service(svcInfo.ServiceName, "", true, &api.QueryOptions{})
+ if err != nil {
+ return fmt.Errorf("failed to check the service registration status: %v", err)
+ }
+
+ registered := false
+ for _, svc := range services {
+ if svc.Service.ID == instanceID {
+ registered = true
+ break
+ }
+ }
+
+ if !registered {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ConsulDiscovery] The service has not been registered. Re-register: %s, instanceID: %s", svcInfo.ServiceName, instanceID))
+ return d.register(ctx, svcInfo.ServiceName, instanceID, svcInfo.Host, svcInfo.HTTPPort, svcInfo.GRPCPort, true)
+ }
+ return nil
+ }
+
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ConsulDiscovery] The re-registration service failed: %v", err))
+ return err
+ }
+ }
+ return nil
+}
+
+// keepAliveLoop .
+func (d *ConsulDiscovery) keepAliveLoop(ctx context.Context, instanceID string) {
+ ticker := time.NewTicker(d.config.keepAlive)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ cli := d.pool.Get()
+ operation := func() error {
+ return cli.Agent().PassTTL("service:"+instanceID, "keepalive")
+ }
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ d.outLog(OutputLogTypeWarn, "[ConsulDiscovery] Failed to update TTL")
+ if err = d.checkAndReRegisterServices(ctx); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ConsulDiscovery] The re-registration service failed: %v", err))
+ }
+ }
+ d.pool.Put(cli)
+ case <-ctx.Done():
+ d.outLog(OutputLogTypeInfo, "[ConsulDiscovery] Stopping keepalive")
+ return
+ }
+ }
+}
+
+// Deregister .
+func (d *ConsulDiscovery) Deregister(ctx context.Context, serviceName, instanceID string) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ operation := func() error {
+ return cli.Agent().ServiceDeregister(instanceID)
+ }
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ return fmt.Errorf("failed to deregister instance: %v", err)
+ }
+
+ d.mutex.Lock()
+ delete(d.registeredServices, instanceID)
+ d.mutex.Unlock()
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[ConsulDiscovery] Deregistered instance, service: %s, instanceId: %s", serviceName, instanceID))
+
+ return nil
+}
+
+// Watch .
+func (d *ConsulDiscovery) Watch(ctx context.Context, serviceName string) (chan []instance.ServiceInstance, error) {
+ ch := make(chan []instance.ServiceInstance, 1)
+ go func() {
+ defer close(ch)
+ lastIndex := uint64(0)
+ for {
+ cli := d.pool.Get()
+ var services []*api.ServiceEntry
+ var meta *api.QueryMeta
+
+ operation := func() error {
+ var queryErr error
+ services, meta, queryErr = cli.Health().Service(serviceName, "", true, &api.QueryOptions{WaitIndex: lastIndex})
+ return queryErr
+ }
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ConsulDiscovery] Failed to query services: %v", err))
+
+ if err = d.checkAndReRegisterServices(ctx); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ConsulDiscovery] The re-registration service failed: %v", err))
+ }
+ d.pool.Put(cli)
+ time.Sleep(time.Second)
+ continue
+ }
+
+ lastIndex = meta.LastIndex
+ instances := d.servicesToInstances(services)
+
+ d.pool.Put(cli)
+
+ select {
+ case ch <- instances:
+ case <-ctx.Done():
+ return
+ }
+
+ time.Sleep(time.Second)
+ }
+ }()
+ return ch, nil
+}
+
+// GetInstances .
+func (d *ConsulDiscovery) GetInstances(serviceName string) ([]instance.ServiceInstance, error) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ var services []*api.ServiceEntry
+ operation := func() error {
+ var queryErr error
+ services, _, queryErr = cli.Health().Service(serviceName, "", true, nil)
+ return queryErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ if err := d.checkAndReRegisterServices(context.Background()); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+
+ return nil, fmt.Errorf("failed to get instances: %v", err)
+ }
+
+ return d.servicesToInstances(services), nil
+}
+
+// servicesToInstances .
+func (d *ConsulDiscovery) servicesToInstances(services []*api.ServiceEntry) []instance.ServiceInstance {
+ var instances []instance.ServiceInstance
+ for _, svc := range services {
+ httpPort := ""
+ grpcPort := ""
+ if port, ok := svc.Service.Meta["http_port"]; ok {
+ httpPort = port
+ }
+ if port, ok := svc.Service.Meta["grpc_port"]; ok {
+ grpcPort = port
+ }
+
+ instances = append(instances, instance.ServiceInstance{
+ InstanceID: svc.Service.ID,
+ Host: svc.Service.Address,
+ Metadata: svc.Service.Meta,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ })
+ }
+ return instances
+}
+
+// Close .
+func (d *ConsulDiscovery) Close() error {
+ d.pool.Close()
+ return nil
+}
diff --git a/servicediscovery/discovery.go b/servicediscovery/discovery.go
new file mode 100644
index 0000000..2515c41
--- /dev/null
+++ b/servicediscovery/discovery.go
@@ -0,0 +1,470 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package servicediscovery
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/helper"
+ "github.com/wenlng/go-service-link/servicediscovery/balancer"
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// registeredServiceInfo Save the information of the registered services
+type registeredServiceInfo struct {
+ ServiceName string
+ InstanceID string
+ Host string
+ HTTPPort string
+ GRPCPort string
+}
+
+// ServiceDiscoveryType .
+type ServiceDiscoveryType string
+
+const (
+ ServiceDiscoveryTypeEtcd ServiceDiscoveryType = "etcd"
+ ServiceDiscoveryTypeZookeeper = "zookeeper"
+ ServiceDiscoveryTypeConsul = "consul"
+ ServiceDiscoveryTypeNacos = "nacos"
+ ServiceDiscoveryTypeNone = "none"
+)
+
+// OutputLogType ..
+type OutputLogType = helper.OutputLogType
+
+const (
+ OutputLogTypeWarn = helper.OutputLogTypeWarn
+ OutputLogTypeInfo = helper.OutputLogTypeInfo
+ OutputLogTypeError = helper.OutputLogTypeError
+ OutputLogTypeDebug = helper.OutputLogTypeDebug
+)
+
+// OutputLogCallback ..
+type OutputLogCallback = helper.OutputLogCallback
+
+// ServiceDiscovery defines the interface for service discovery
+type ServiceDiscovery interface {
+ Register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string) error
+ Deregister(ctx context.Context, serviceName, instanceID string) error
+ Watch(ctx context.Context, serviceName string) (chan []instance.ServiceInstance, error)
+ GetInstances(serviceName string) ([]instance.ServiceInstance, error)
+ SetOutputLogCallback(outputLogCallback OutputLogCallback)
+ Close() error
+}
+
+// Config .
+type Config struct {
+ Type ServiceDiscoveryType // etcd, zookeeper, consul, nacos, none
+ ServiceName string
+
+ // CommonBase
+ Addrs string // 127.0.0.1:8080,192.168.0.1:8080
+ PoolSize int
+ TTL time.Duration // TTL
+ KeepAlive time.Duration // Heartbeat interval
+ MaxRetries int
+ BaseRetryDelay time.Duration
+ TlsConfig *common.TLSConfig
+ Username string
+ Password string
+
+ // Health Check
+ HealthCheckConfig
+
+ // Extra Config
+ ConsulDiscoveryConfig
+ EtcdDiscoveryConfig
+ NacosDiscoveryConfig
+ ZooKeeperDiscoveryConfig
+}
+
+// NewServiceDiscovery .
+func NewServiceDiscovery(config Config) (ServiceDiscovery, error) {
+ var discovery ServiceDiscovery
+ var err error
+ switch config.Type {
+ case ServiceDiscoveryTypeEtcd:
+ cnf := config.EtcdDiscoveryConfig
+
+ cnf.tlsConfig = config.TlsConfig
+ cnf.poolSize = config.PoolSize
+ cnf.address = strings.Split(config.Addrs, ",")
+ cnf.ttl = config.TTL
+ cnf.keepAlive = config.KeepAlive
+ cnf.maxRetries = config.MaxRetries
+ cnf.baseRetryDelay = config.BaseRetryDelay
+ cnf.tlsConfig = config.TlsConfig
+ cnf.username = config.Username
+ cnf.password = config.Password
+
+ discovery, err = NewEtcdDiscovery(cnf)
+ case ServiceDiscoveryTypeZookeeper:
+ cnf := config.ZooKeeperDiscoveryConfig
+
+ cnf.tlsConfig = config.TlsConfig
+ cnf.poolSize = config.PoolSize
+ cnf.address = strings.Split(config.Addrs, ",")
+ cnf.ttl = config.TTL
+ cnf.keepAlive = config.KeepAlive
+ cnf.maxRetries = config.MaxRetries
+ cnf.baseRetryDelay = config.BaseRetryDelay
+ cnf.tlsConfig = config.TlsConfig
+ cnf.username = config.Username
+ cnf.password = config.Password
+
+ discovery, err = NewZooKeeperDiscovery(cnf)
+ case ServiceDiscoveryTypeConsul:
+ cnf := config.ConsulDiscoveryConfig
+
+ cnf.tlsConfig = config.TlsConfig
+ cnf.poolSize = config.PoolSize
+ cnf.address = strings.Split(config.Addrs, ",")
+ cnf.ttl = config.TTL
+ cnf.keepAlive = config.KeepAlive
+ cnf.maxRetries = config.MaxRetries
+ cnf.baseRetryDelay = config.BaseRetryDelay
+ cnf.tlsConfig = config.TlsConfig
+ cnf.username = config.Username
+ cnf.password = config.Password
+
+ discovery, err = NewConsulDiscovery(cnf)
+ case ServiceDiscoveryTypeNacos:
+ cnf := config.NacosDiscoveryConfig
+
+ cnf.tlsConfig = config.TlsConfig
+ cnf.poolSize = config.PoolSize
+ cnf.address = strings.Split(config.Addrs, ",")
+ cnf.ttl = config.TTL
+ cnf.keepAlive = config.KeepAlive
+ cnf.maxRetries = config.MaxRetries
+ cnf.baseRetryDelay = config.BaseRetryDelay
+ cnf.tlsConfig = config.TlsConfig
+ cnf.username = config.Username
+ cnf.password = config.Password
+
+ discovery, err = NewNacosDiscovery(cnf)
+ case ServiceDiscoveryTypeNone:
+ discovery = &NoopDiscovery{}
+ default:
+ return nil, fmt.Errorf("unsupported service discovery type: %s", config.Type)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return discovery, nil
+}
+
+// HealthCheckConfig .
+type HealthCheckConfig struct {
+ Interval time.Duration // How often to check
+ Timeout time.Duration // Timeout for each check
+ MaxFailedChecks int // Max failed checks before marking unhealthy
+ MinHealthyTime time.Duration // Minimum time to consider instance healthy
+}
+
+// DiscoveryWithLB .
+type DiscoveryWithLB struct {
+ discovery ServiceDiscovery
+ lb balancer.LoadBalancer
+ instances map[string][]instance.ServiceInstance
+ insMu sync.RWMutex
+
+ healthCheckConfig HealthCheckConfig // Health check configuration
+ healthCheckStop map[string]chan struct{} // Channels to stop health checks
+ hcsMu sync.RWMutex
+}
+
+// NewDiscoveryWithLB .
+func NewDiscoveryWithLB(config Config, lbType balancer.LoadBalancerType) (*DiscoveryWithLB, error) {
+ discovery, err := NewServiceDiscovery(config)
+ if err != nil {
+ return nil, err
+ }
+ var lb balancer.LoadBalancer
+ switch lbType {
+ case balancer.LoadBalancerTypeRandom:
+ lb = balancer.NewRandomBalancer()
+ case balancer.LoadBalancerTypeRoundRobin:
+ lb = balancer.NewRoundRobinBalancer()
+ case balancer.LoadBalancerTypeConsistentHash:
+ lb = balancer.NewConsistentHashBalancer()
+ default:
+ return nil, fmt.Errorf("unsupported load balancer type: %s", lbType)
+ }
+
+ if !helper.IsDurationSet(config.HealthCheckConfig.Interval) {
+ config.HealthCheckConfig.Interval = 5 * time.Second
+ }
+ if !helper.IsDurationSet(config.HealthCheckConfig.Timeout) {
+ config.HealthCheckConfig.Timeout = 2 * time.Second
+ }
+ if !helper.IsDurationSet(config.HealthCheckConfig.MinHealthyTime) {
+ config.HealthCheckConfig.MinHealthyTime = 30 * time.Second
+ }
+ if config.HealthCheckConfig.MaxFailedChecks <= 0 {
+ config.HealthCheckConfig.MaxFailedChecks = 3
+ }
+
+ dlb := &DiscoveryWithLB{
+ discovery: discovery,
+ lb: lb,
+ instances: make(map[string][]instance.ServiceInstance),
+ healthCheckConfig: HealthCheckConfig{
+ Interval: config.HealthCheckConfig.Interval,
+ Timeout: config.HealthCheckConfig.Timeout,
+ MaxFailedChecks: config.HealthCheckConfig.MaxFailedChecks,
+ MinHealthyTime: config.HealthCheckConfig.MinHealthyTime,
+ },
+ healthCheckStop: make(map[string]chan struct{}),
+ }
+ return dlb, nil
+}
+
+// SetOutputLogCallback .
+func (d *DiscoveryWithLB) SetOutputLogCallback(outputLogCallback OutputLogCallback) {
+ d.discovery.SetOutputLogCallback(outputLogCallback)
+}
+
+// Register .
+func (d *DiscoveryWithLB) Register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string) error {
+ err := d.discovery.Register(ctx, serviceName, instanceID, host, httpPort, grpcPort)
+ if err == nil {
+ d.startHealthCheck(ctx, serviceName, instanceID, host, httpPort)
+ }
+ return err
+}
+
+// Deregister .
+func (d *DiscoveryWithLB) Deregister(ctx context.Context, serviceName, instanceID string) error {
+ d.stopHealthCheck(serviceName, instanceID)
+ return d.discovery.Deregister(ctx, serviceName, instanceID)
+}
+
+// Watch .
+func (d *DiscoveryWithLB) Watch(ctx context.Context, serviceName string) (chan []instance.ServiceInstance, error) {
+ ch, err := d.discovery.Watch(ctx, serviceName)
+ if err != nil {
+ return nil, err
+ }
+ outCh := make(chan []instance.ServiceInstance)
+ go func() {
+ for instances := range ch {
+ d.insMu.Lock()
+ // Update existing instances with health status
+ updatedInstances := make([]instance.ServiceInstance, len(instances))
+ for i, inst := range instances {
+ existing, exists := d.findInstance(serviceName, inst.InstanceID)
+ if exists && existing.IsHealthy {
+ inst.IsHealthy = existing.IsHealthy
+ inst.LastChecked = existing.LastChecked
+ inst.FailedChecks = existing.FailedChecks
+ } else {
+ inst.IsHealthy = true
+ inst.LastChecked = time.Now()
+ }
+ updatedInstances[i] = inst
+ if !exists {
+ if inst.HTTPPort != "" {
+ d.startHealthCheck(ctx, serviceName, inst.InstanceID, inst.Host, inst.HTTPPort)
+ }
+ if inst.GRPCPort != "" {
+ d.startHealthCheck(ctx, serviceName, inst.InstanceID, inst.Host, inst.GRPCPort)
+ }
+ }
+ }
+ d.instances[serviceName] = updatedInstances
+ d.insMu.Unlock()
+ outCh <- updatedInstances
+ }
+ close(outCh)
+ }()
+
+ return outCh, nil
+}
+
+// GetInstances .
+func (d *DiscoveryWithLB) GetInstances(serviceName string) ([]instance.ServiceInstance, error) {
+ d.insMu.RLock()
+ instances, exists := d.instances[serviceName]
+ d.insMu.RUnlock()
+ if exists {
+ return d.filterHealthyInstances(instances), nil
+ }
+
+ instances, err := d.discovery.GetInstances(serviceName)
+ if err != nil {
+ return nil, err
+ }
+
+ d.insMu.Lock()
+ for i := range instances {
+ instances[i].IsHealthy = true
+ instances[i].LastChecked = time.Now()
+
+ if instances[i].HTTPPort != "" {
+ d.startHealthCheck(context.Background(), serviceName, instances[i].InstanceID, instances[i].Host, instances[i].HTTPPort)
+ }
+ if instances[i].GRPCPort != "" {
+ d.startHealthCheck(context.Background(), serviceName, instances[i].InstanceID, instances[i].Host, instances[i].GRPCPort)
+ }
+ }
+ d.instances[serviceName] = instances
+ d.insMu.Unlock()
+
+ return d.filterHealthyInstances(instances), nil
+}
+
+// Select .
+func (d *DiscoveryWithLB) Select(serviceName, key string) (instance.ServiceInstance, error) {
+ instances, err := d.GetInstances(serviceName)
+ if err != nil {
+ return instance.ServiceInstance{}, err
+ }
+ if len(instances) == 0 {
+ return instance.ServiceInstance{}, fmt.Errorf("no instances available for service: %s", serviceName)
+ }
+ return d.lb.Select(instances, key)
+}
+
+// Close .
+func (d *DiscoveryWithLB) Close() error {
+ for serviceName := range d.healthCheckStop {
+ d.stopAllHealthChecks(serviceName)
+ }
+
+ return d.discovery.Close()
+}
+
+// startHealthCheck begins periodic health checking for an instance
+func (d *DiscoveryWithLB) startHealthCheck(ctx context.Context, serviceName, instanceID, host, httpPort string) {
+ key := serviceName + "-" + instanceID
+ stopChan := make(chan struct{})
+ d.hcsMu.Lock()
+ if _, ok := d.healthCheckStop[key]; ok {
+ d.hcsMu.Unlock()
+ return
+ }
+ d.healthCheckStop[key] = stopChan
+ d.hcsMu.Unlock()
+
+ go func() {
+ ticker := time.NewTicker(d.healthCheckConfig.Interval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-stopChan:
+ return
+ case <-ticker.C:
+ isHealthy := d.performHealthCheck(host, httpPort)
+ var hasInts bool
+ d.insMu.Lock()
+ instances, exists := d.instances[serviceName]
+ if exists {
+ for i, inst := range instances {
+ if inst.InstanceID == instanceID {
+ if isHealthy {
+ instances[i].FailedChecks = 0
+ instances[i].IsHealthy = true
+ } else {
+ instances[i].FailedChecks++
+ if instances[i].FailedChecks >= d.healthCheckConfig.MaxFailedChecks {
+ instances[i].IsHealthy = false
+ }
+ }
+ instances[i].LastChecked = time.Now()
+ d.instances[serviceName] = instances
+ hasInts = true
+ break
+ }
+ }
+ }
+ d.insMu.Unlock()
+
+ if !hasInts {
+ d.hcsMu.Lock()
+ if _, ok := d.healthCheckStop[key]; ok {
+ close(d.healthCheckStop[key])
+ delete(d.healthCheckStop, key)
+ }
+ d.hcsMu.Unlock()
+ }
+ }
+ }
+ }()
+}
+
+// performHealthCheck ..
+func (d *DiscoveryWithLB) performHealthCheck(host, httpPort string) bool {
+ _, cancel := context.WithTimeout(context.Background(), d.healthCheckConfig.Timeout)
+ defer cancel()
+
+ conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, httpPort), d.healthCheckConfig.Timeout)
+ if err != nil {
+ return false
+ }
+ defer conn.Close()
+
+ return true
+}
+
+// stopHealthCheck ..
+func (d *DiscoveryWithLB) stopHealthCheck(serviceName, instanceID string) {
+ d.hcsMu.Lock()
+ if stopChan, exists := d.healthCheckStop[serviceName+"-"+instanceID]; exists {
+ close(stopChan)
+ delete(d.healthCheckStop, serviceName+"-"+instanceID)
+ }
+ d.hcsMu.Unlock()
+}
+
+// stopAllHealthChecks ..
+func (d *DiscoveryWithLB) stopAllHealthChecks(serviceName string) {
+ d.hcsMu.Lock()
+ for key, stopChan := range d.healthCheckStop {
+ if strings.HasPrefix(key, serviceName+"-") {
+ close(stopChan)
+ delete(d.healthCheckStop, key)
+ }
+ }
+ d.hcsMu.Unlock()
+}
+
+// findInstance ..
+func (d *DiscoveryWithLB) findInstance(serviceName, instanceID string) (instance.ServiceInstance, bool) {
+ instances, exists := d.instances[serviceName]
+ if !exists {
+ return instance.ServiceInstance{}, false
+ }
+ for _, inst := range instances {
+ if inst.InstanceID == instanceID {
+ return inst, true
+ }
+ }
+ return instance.ServiceInstance{}, false
+}
+
+// filterHealthyInstances ..
+func (d *DiscoveryWithLB) filterHealthyInstances(instances []instance.ServiceInstance) []instance.ServiceInstance {
+ var healthy []instance.ServiceInstance
+ for _, inst := range instances {
+ if inst.IsHealthy && time.Since(inst.LastChecked) < d.healthCheckConfig.MinHealthyTime {
+ healthy = append(healthy, inst)
+ }
+ }
+ return healthy
+}
diff --git a/servicediscovery/discovery_test.go b/servicediscovery/discovery_test.go
new file mode 100644
index 0000000..b0abfef
--- /dev/null
+++ b/servicediscovery/discovery_test.go
@@ -0,0 +1,133 @@
+package servicediscovery
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEtcdDiscovery(t *testing.T) {
+ discovery, err := NewServiceDiscovery(Config{
+ Type: ServiceDiscoveryTypeEtcd,
+ ServiceName: "hello-app",
+ Addrs: "localhost:2379",
+ })
+ if err != nil {
+ t.Skip("etcd not available")
+ }
+ defer discovery.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ serviceName := "test-service"
+ instanceID := uuid.New().String()
+ addr := "localhost:8080"
+ httpPort := "8080"
+ grpcPort := "50051"
+
+ t.Run("Register", func(t *testing.T) {
+ err := discovery.Register(ctx, serviceName, instanceID, addr, httpPort, grpcPort)
+ assert.NoError(t, err)
+
+ instances, err := discovery.GetInstances(serviceName)
+ assert.NoError(t, err)
+ assert.Len(t, instances, 1)
+ assert.Equal(t, instanceID, instances[0].InstanceID)
+ assert.Equal(t, addr, instances[0].Host)
+ assert.Equal(t, httpPort, instances[0].Metadata["http_port"])
+ assert.Equal(t, grpcPort, instances[0].Metadata["grpc_port"])
+ })
+
+ t.Run("Watch", func(t *testing.T) {
+ ch, err := discovery.Watch(ctx, serviceName)
+ assert.NoError(t, err)
+
+ select {
+ case instances := <-ch:
+ assert.Len(t, instances, 1)
+ assert.Equal(t, instanceID, instances[0].InstanceID)
+ case <-time.After(time.Second * 5):
+ t.Fatal("Watch timeout")
+ }
+ })
+
+ t.Run("Deregister", func(t *testing.T) {
+ err := discovery.Deregister(ctx, serviceName, instanceID)
+ assert.NoError(t, err)
+
+ instances, err := discovery.GetInstances(serviceName)
+ assert.NoError(t, err)
+ assert.Len(t, instances, 0)
+ })
+}
+
+func TestDiscoveryWithLB(t *testing.T) {
+ config := Config{
+ Type: "etcd",
+ Addrs: "localhost:2379",
+ TTL: time.Second * 10,
+ KeepAlive: time.Second * 3,
+ ServiceName: "test-service",
+ }
+ dlb, err := NewDiscoveryWithLB(config, "round_robin")
+ if err != nil {
+ t.Skip("etcd not available")
+ }
+ defer dlb.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ serviceName := "test-service"
+ instanceID1 := uuid.New().String()
+ instanceID2 := uuid.New().String()
+ addr1 := "localhost:8081"
+ addr2 := "localhost:8082"
+ httpPort := "8080"
+ grpcPort := "50051"
+
+ t.Run("RegisterAndSelect", func(t *testing.T) {
+ err := dlb.Register(ctx, serviceName, instanceID1, addr1, httpPort, grpcPort)
+ assert.NoError(t, err)
+ err = dlb.Register(ctx, serviceName, instanceID2, addr2, httpPort, grpcPort)
+ assert.NoError(t, err)
+
+ for i := 0; i < 4; i++ {
+ inst, err := dlb.Select(serviceName, fmt.Sprintf("key%d", i))
+ assert.NoError(t, err)
+ assert.Contains(t, []string{addr1, addr2}, inst.Host)
+ assert.Equal(t, httpPort, inst.Metadata["http_port"])
+ assert.Equal(t, grpcPort, inst.Metadata["grpc_port"])
+ }
+ })
+
+ t.Run("Watch", func(t *testing.T) {
+ ch, err := dlb.Watch(ctx, serviceName)
+ assert.NoError(t, err)
+
+ select {
+ case instances := <-ch:
+ assert.Len(t, instances, 2)
+ addrs := []string{instances[0].Host, instances[1].Host}
+ assert.Contains(t, addrs, addr1)
+ assert.Contains(t, addrs, addr2)
+ case <-time.After(time.Second * 5):
+ t.Fatal("Watch timeout")
+ }
+ })
+
+ t.Run("Deregister", func(t *testing.T) {
+ err := dlb.Deregister(ctx, serviceName, instanceID1)
+ assert.NoError(t, err)
+
+ instances, err := dlb.GetInstances(serviceName)
+ assert.NoError(t, err)
+ assert.Len(t, instances, 1)
+ assert.Equal(t, instanceID2, instances[0].InstanceID)
+ })
+}
diff --git a/servicediscovery/etcd_discovery.go b/servicediscovery/etcd_discovery.go
new file mode 100644
index 0000000..4c26b88
--- /dev/null
+++ b/servicediscovery/etcd_discovery.go
@@ -0,0 +1,434 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package servicediscovery
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "path"
+ "sync"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+ "go.etcd.io/etcd/client/v3"
+)
+
+// EtcdDiscovery .
+type EtcdDiscovery struct {
+ leaseID clientv3.LeaseID
+ keepAliveCh <-chan *clientv3.LeaseKeepAliveResponse
+ pool *clientpool.EtcdPool
+ outputLogCallback OutputLogCallback
+ config EtcdDiscoveryConfig
+
+ registeredServices map[string]registeredServiceInfo
+ mutex sync.RWMutex
+}
+
+// EtcdDiscoveryConfig .
+type EtcdDiscoveryConfig struct {
+ extraconfig.EtcdExtraConfig
+
+ address []string
+ poolSize int
+ ttl time.Duration
+ keepAlive time.Duration
+ maxRetries int
+ baseRetryDelay time.Duration
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+}
+
+// NewEtcdDiscovery .
+func NewEtcdDiscovery(config EtcdDiscoveryConfig) (*EtcdDiscovery, error) {
+ if config.poolSize <= 0 {
+ config.poolSize = 5
+ }
+
+ if config.maxRetries <= 0 {
+ config.maxRetries = 3
+ }
+
+ if config.ttl <= 0 {
+ config.ttl = 10 * time.Second
+ }
+
+ if !helper.IsDurationSet(config.baseRetryDelay) {
+ config.baseRetryDelay = 500 * time.Millisecond
+ }
+
+ config.EtcdExtraConfig.Username = config.username
+ config.EtcdExtraConfig.Password = config.password
+
+ pool, err := clientpool.NewEtcdPool(config.poolSize, config.address, &config.EtcdExtraConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return &EtcdDiscovery{
+ config: config,
+ pool: pool,
+ registeredServices: make(map[string]registeredServiceInfo),
+ }, nil
+}
+
+// SetOutputLogCallback .
+func (d *EtcdDiscovery) SetOutputLogCallback(outputLogCallback OutputLogCallback) {
+ d.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (d *EtcdDiscovery) outLog(logType OutputLogType, message string) {
+ if d.outputLogCallback != nil {
+ d.outputLogCallback(logType, message)
+ }
+}
+
+// checkAndReRegisterServices check and re-register the service
+func (d *EtcdDiscovery) checkAndReRegisterServices(ctx context.Context) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ d.mutex.RLock()
+ services := make(map[string]registeredServiceInfo)
+ for k, v := range d.registeredServices {
+ services[k] = v
+ }
+ d.mutex.RUnlock()
+
+ for instanceID, svcInfo := range services {
+ key := path.Join("/services", svcInfo.ServiceName, instanceID)
+ operation := func() error {
+ resp, err := cli.Get(ctx, key)
+ if err != nil {
+ return fmt.Errorf("failed to check the service registration status: %v", err)
+ }
+ if len(resp.Kvs) == 0 {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The service has not been registered. Re-register: %s, instanceID: %s", svcInfo.ServiceName, instanceID))
+ return d.register(ctx, svcInfo.ServiceName, instanceID, svcInfo.Host, svcInfo.HTTPPort, svcInfo.GRPCPort, true)
+ }
+ return nil
+ }
+
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ return err
+ }
+ }
+ return nil
+}
+
+// reRegister .
+func (d *EtcdDiscovery) register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string, isReRegister bool) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+ if instanceID == "" {
+ instanceID = uuid.New().String()
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[EtcdDiscovery] The registration service is beginning...., service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+
+ if !isReRegister {
+ d.mutex.Lock()
+ d.registeredServices[instanceID] = registeredServiceInfo{
+ ServiceName: serviceName,
+ InstanceID: instanceID,
+ Host: host,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ }
+ d.mutex.Unlock()
+
+ go d.watchKeepAlive(ctx)
+ }
+
+ var leaseResp *clientv3.LeaseGrantResponse
+ operation := func() error {
+ var grantErr error
+ leaseResp, grantErr = cli.Grant(ctx, int64(d.config.ttl/time.Second))
+ return grantErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ return fmt.Errorf("failed to grant lease: %v", err)
+ }
+
+ d.leaseID = leaseResp.ID
+ data, err := json.Marshal(instance.ServiceInstance{
+ InstanceID: instanceID,
+ Host: host,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ Metadata: map[string]string{
+ "hostname": helper.GetHostname(),
+ "host": host,
+ "http_port": httpPort,
+ "grpc_port": grpcPort,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("failed to marshal instance: %v", err)
+ }
+
+ key := path.Join("/services", serviceName, instanceID)
+ operation = func() error {
+ _, putErr := cli.Put(ctx, key, string(data), clientv3.WithLease(d.leaseID))
+ return putErr
+ }
+ if err = helper.WithRetry(context.Background(), operation); err != nil {
+ return fmt.Errorf("failed to register instance: %v", err)
+ }
+
+ operation = func() error {
+ var keepAliveErr error
+ d.keepAliveCh, keepAliveErr = cli.KeepAlive(ctx, d.leaseID)
+ return keepAliveErr
+ }
+ if err = helper.WithRetry(context.Background(), operation); err != nil {
+ return fmt.Errorf("failed to start keepalive: %v", err)
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[EtcdDiscovery] Registered instance, service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+
+ return nil
+}
+
+// Register .
+func (d *EtcdDiscovery) Register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string) error {
+ return d.register(ctx, serviceName, instanceID, host, httpPort, grpcPort, false)
+}
+
+// watchKeepAlive .
+func (d *EtcdDiscovery) watchKeepAlive(ctx context.Context) {
+ for {
+ select {
+ case _, ok := <-d.keepAliveCh:
+ if !ok {
+ d.outLog(OutputLogTypeWarn, "KeepAlive channel closed")
+ go d.recoverKeepAlive(ctx)
+ return
+ }
+ case <-ctx.Done():
+ d.outLog(OutputLogTypeWarn, "Stopping keepalive")
+ return
+ }
+ }
+}
+
+// recoverKeepAlive try to restore the heartbeat
+func (d *EtcdDiscovery) recoverKeepAlive(ctx context.Context) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ if d.keepAliveCh != nil {
+ d.keepAliveCh = nil
+ }
+
+ operation := func() error {
+ var keepAliveErr error
+ d.keepAliveCh, keepAliveErr = cli.KeepAlive(ctx, d.leaseID)
+ return keepAliveErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[EtcdDiscovery] Failed to restore the Etcd heartbeat: %v", err))
+ return
+ }
+
+ if err := d.checkAndReRegisterServices(ctx); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+
+ go d.watchKeepAlive(ctx)
+ d.outLog(OutputLogTypeInfo, "The heart rate of Ectd was successfully restored")
+}
+
+// Deregister .
+func (d *EtcdDiscovery) Deregister(ctx context.Context, serviceName, instanceID string) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ key := path.Join("/services", serviceName, instanceID)
+ operation := func() error {
+ _, deleteErr := cli.Delete(ctx, key)
+ return deleteErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ return fmt.Errorf("failed to deregister instance: %v", err)
+ }
+
+ if d.leaseID != 0 {
+ operation = func() error {
+ _, revokeErr := cli.Revoke(ctx, d.leaseID)
+ return revokeErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[EtcdDiscovery] Failed to revoke lease: %v", err))
+ }
+ }
+
+ d.mutex.Lock()
+ delete(d.registeredServices, instanceID)
+ d.mutex.Unlock()
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[EtcdDiscovery] Deregistered instance, service: %s, instanceId: %s", serviceName, instanceID))
+
+ return nil
+}
+
+// Watch .
+func (d *EtcdDiscovery) Watch(ctx context.Context, serviceName string) (chan []instance.ServiceInstance, error) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ prefix := path.Join("/services", serviceName)
+ ch := make(chan []instance.ServiceInstance, 1)
+ rch := cli.Watch(ctx, prefix, clientv3.WithPrefix())
+ go func() {
+ defer close(ch)
+ for resp := range rch {
+ instances, err := d.getInstancesFromEvents(serviceName, resp.Events)
+ if err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[EtcdDiscovery] Failed to parse watch events: %v", err))
+ go d.recoverWatch(ctx, serviceName, ch)
+ continue
+ }
+
+ select {
+ case ch <- instances:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ instances, err := d.GetInstances(serviceName)
+ if err != nil {
+ return nil, err
+ }
+ if len(instances) > 0 {
+ ch <- instances
+ }
+ return ch, nil
+}
+
+// recoverWatch attempt to restore surveillance
+func (d *EtcdDiscovery) recoverWatch(ctx context.Context, serviceName string, ch chan []instance.ServiceInstance) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ prefix := path.Join("/services", serviceName)
+ rch := cli.Watch(ctx, prefix, clientv3.WithPrefix())
+
+ if err := d.checkAndReRegisterServices(ctx); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+
+ go func() {
+ for resp := range rch {
+ if resp.Err() != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[EtcdDiscovery] Monitor Etcd event errors: %v", resp.Err()))
+ continue
+ }
+ instances, err := d.getInstancesFromEvents(serviceName, resp.Events)
+ if err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[EtcdDiscovery] The Etcd monitoring event cannot be parsed: %v", err))
+ continue
+ }
+
+ select {
+ case ch <- instances:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ d.outLog(OutputLogTypeInfo, "The ETCD monitoring was successfully restored")
+}
+
+// GetInstances .
+func (d *EtcdDiscovery) GetInstances(serviceName string) ([]instance.ServiceInstance, error) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ prefix := path.Join("/services", serviceName)
+
+ var resp *clientv3.GetResponse
+ operation := func() error {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ var getErr error
+ resp, getErr = cli.Get(ctx, prefix, clientv3.WithPrefix())
+ return getErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ if err = d.checkAndReRegisterServices(context.Background()); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+ return nil, fmt.Errorf("failed to get instances: %v", err)
+ }
+
+ var instances []instance.ServiceInstance
+ for _, kv := range resp.Kvs {
+ var inst instance.ServiceInstance
+ if err := json.Unmarshal(kv.Value, &inst); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[EtcdDiscovery] Failed to unmarshal instance: %v", err))
+ continue
+ }
+
+ httpPort := ""
+ grpcPort := ""
+ if port, ok := inst.Metadata["http_port"]; ok {
+ httpPort = port
+ }
+ if port, ok := inst.Metadata["grpc_port"]; ok {
+ grpcPort = port
+ }
+
+ inst.HTTPPort = httpPort
+ inst.GRPCPort = grpcPort
+ instances = append(instances, inst)
+ }
+ return instances, nil
+}
+
+// getInstancesFromEvents .
+func (d *EtcdDiscovery) getInstancesFromEvents(serviceName string, events []*clientv3.Event) ([]instance.ServiceInstance, error) {
+ instances, err := d.GetInstances(serviceName)
+ if err != nil {
+ return nil, err
+ }
+ return instances, nil
+}
+
+// Close .
+func (d *EtcdDiscovery) Close() error {
+ cli := d.pool.Get()
+
+ if d.leaseID != 0 {
+ _, err := cli.Revoke(context.Background(), d.leaseID)
+ if err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[EtcdDiscovery] Failed to revoke lease on close: %v", err))
+ }
+ }
+
+ d.pool.Close()
+ return nil
+}
diff --git a/servicediscovery/instance/instance.go b/servicediscovery/instance/instance.go
new file mode 100644
index 0000000..b1b1112
--- /dev/null
+++ b/servicediscovery/instance/instance.go
@@ -0,0 +1,63 @@
+package instance
+
+import (
+ "fmt"
+ "time"
+)
+
+// ServiceInstance represents a service instance
+type ServiceInstance struct {
+ InstanceID string
+ Host string
+ HTTPPort string
+ GRPCPort string
+ Metadata map[string]string
+
+ IsHealthy bool
+ LastChecked time.Time
+ FailedChecks int
+}
+
+// GetHost .
+func (si *ServiceInstance) GetHost() string {
+ if si.Host != "" {
+ return si.Host
+ }
+ return "127.0.0.1"
+}
+
+// GetHTTPPort .
+func (si *ServiceInstance) GetHTTPPort() string {
+ if si.HTTPPort != "" {
+ return si.HTTPPort
+ }
+
+ if port, ok := si.Metadata["http_port"]; ok {
+ return port
+ }
+
+ return "80"
+}
+
+// GetGRPCPort .
+func (si *ServiceInstance) GetGRPCPort() string {
+ if si.GRPCPort != "" {
+ return si.GRPCPort
+ }
+
+ if port, ok := si.Metadata["grpc_port"]; ok {
+ return port
+ }
+
+ return "50051"
+}
+
+// GetHTTPAddress .
+func (si *ServiceInstance) GetHTTPAddress() string {
+ return fmt.Sprintf("%s:%s", si.GetHost(), si.GetHTTPPort())
+}
+
+// GetGRPCAddress .
+func (si *ServiceInstance) GetGRPCAddress() string {
+ return fmt.Sprintf("%s:%s", si.GetHost(), si.GetGRPCPort())
+}
diff --git a/servicediscovery/nacos_discovery.go b/servicediscovery/nacos_discovery.go
new file mode 100644
index 0000000..5161efe
--- /dev/null
+++ b/servicediscovery/nacos_discovery.go
@@ -0,0 +1,417 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package servicediscovery
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/nacos-group/nacos-sdk-go/v2/model"
+ "github.com/nacos-group/nacos-sdk-go/v2/vo"
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// NacosDiscovery .
+type NacosDiscovery struct {
+ outputLogCallback OutputLogCallback
+ pool *clientpool.NacosNamingPool
+ clientConfig NacosDiscoveryConfig
+
+ registeredServices map[string]registeredServiceInfo
+ mutex sync.RWMutex
+}
+
+// NacosDiscoveryConfig .
+type NacosDiscoveryConfig struct {
+ extraconfig.NacosExtraConfig
+
+ address []string
+ poolSize int
+ ttl time.Duration
+ keepAlive time.Duration
+ maxRetries int
+ baseRetryDelay time.Duration
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+}
+
+// NewNacosDiscovery .
+func NewNacosDiscovery(clientConfig NacosDiscoveryConfig) (*NacosDiscovery, error) {
+ if clientConfig.poolSize <= 0 {
+ clientConfig.poolSize = 5
+ }
+
+ if clientConfig.maxRetries <= 0 {
+ clientConfig.maxRetries = 3
+ }
+
+ if !helper.IsDurationSet(clientConfig.baseRetryDelay) {
+ clientConfig.baseRetryDelay = 500 * time.Millisecond
+ }
+
+ clientConfig.NacosExtraConfig.Username = clientConfig.username
+ clientConfig.NacosExtraConfig.Password = clientConfig.password
+
+ pool, err := clientpool.NewNacosNamingPool(clientConfig.poolSize, clientConfig.address, &clientConfig.NacosExtraConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return &NacosDiscovery{
+ clientConfig: clientConfig,
+ pool: pool,
+ registeredServices: make(map[string]registeredServiceInfo),
+ }, nil
+}
+
+// SetOutputLogCallback .
+func (d *NacosDiscovery) SetOutputLogCallback(outputLogCallback OutputLogCallback) {
+ d.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (d *NacosDiscovery) outLog(logType OutputLogType, message string) {
+ if d.outputLogCallback != nil {
+ d.outputLogCallback(logType, message)
+ }
+}
+
+// checkAndReRegisterServices check and re-register the service
+func (d *NacosDiscovery) checkAndReRegisterServices(ctx context.Context) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ d.mutex.RLock()
+ services := make(map[string]registeredServiceInfo)
+ for k, v := range d.registeredServices {
+ services[k] = v
+ }
+ d.mutex.RUnlock()
+
+ for instanceID, svcInfo := range services {
+ operation := func() error {
+ service, err := cli.GetService(vo.GetServiceParam{
+ ServiceName: svcInfo.ServiceName,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to check the service registration status: %v", err)
+ }
+
+ registered := false
+ for _, inst := range service.Hosts {
+ if inst.InstanceId == instanceID {
+ registered = true
+ break
+ }
+ }
+
+ if !registered {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The service has not been registered. Re-register: %s, instanceID: %s", svcInfo.ServiceName, instanceID))
+ return d.register(ctx, svcInfo.ServiceName, instanceID, svcInfo.Host, svcInfo.HTTPPort, svcInfo.GRPCPort, true)
+ }
+ return nil
+ }
+
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ return err
+ }
+ }
+ return nil
+}
+
+// register .
+func (d *NacosDiscovery) register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string, isReRegister bool) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ if instanceID == "" {
+ instanceID = uuid.New().String()
+ }
+
+ if !isReRegister {
+ d.mutex.Lock()
+ d.registeredServices[instanceID] = registeredServiceInfo{
+ ServiceName: serviceName,
+ InstanceID: instanceID,
+ Host: host,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ }
+ d.mutex.Unlock()
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[ConsulDiscovery] The registration service is beginning...., service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+
+ port, _ := strconv.Atoi(httpPort)
+ var success bool
+ operation := func() error {
+ var err error
+ success, err = cli.RegisterInstance(vo.RegisterInstanceParam{
+ Ip: host,
+ Port: uint64(port),
+ ServiceName: serviceName,
+ Weight: 1,
+ Enable: true,
+ Healthy: true,
+ Ephemeral: true,
+ Metadata: map[string]string{
+ "hostname": helper.GetHostname(),
+ "host": host,
+ "http_port": httpPort,
+ "grpc_port": grpcPort,
+ "instance_id": instanceID,
+ },
+ })
+ return err
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ if err = d.checkAndReRegisterServices(ctx); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+ }
+
+ if !success {
+ return fmt.Errorf("RegisterInstance failed with success=false, serviceName: %s", serviceName)
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[NacosDiscovery] Registered instance, service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+ return nil
+}
+
+// Register .
+func (d *NacosDiscovery) Register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string) error {
+ return d.register(ctx, serviceName, instanceID, host, httpPort, grpcPort, false)
+}
+
+// Deregister .
+func (d *NacosDiscovery) Deregister(ctx context.Context, serviceName, instanceID string) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ instances, err := d.GetInstances(serviceName)
+ if err != nil {
+ return fmt.Errorf("failed to get instances for deregister: %v", err)
+ }
+ var curInst model.Instance
+ for _, inst := range instances {
+ if inst.InstanceID == instanceID {
+ port, _ := strconv.Atoi(inst.HTTPPort)
+ curInst = model.Instance{
+ InstanceId: instanceID,
+ Ip: inst.Host,
+ Port: uint64(port),
+ ServiceName: serviceName,
+ }
+ break
+ }
+ }
+
+ var success bool
+ operation := func() error {
+ success, err = cli.DeregisterInstance(vo.DeregisterInstanceParam{
+ Ip: curInst.Ip,
+ Port: curInst.Port,
+ ServiceName: serviceName,
+ Ephemeral: true,
+ })
+ return err
+ }
+ if err = helper.WithRetry(context.Background(), operation); !success || err != nil {
+ return fmt.Errorf("failed to deregister curInst: %v", err)
+ }
+
+ d.mutex.Lock()
+ delete(d.registeredServices, instanceID)
+ d.mutex.Unlock()
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[NacosDiscovery] Deregistered curInst, service: %s, instanceId: %s", serviceName, instanceID))
+
+ return nil
+}
+
+// Watch .
+func (d *NacosDiscovery) Watch(ctx context.Context, serviceName string) (chan []instance.ServiceInstance, error) {
+ ch := make(chan []instance.ServiceInstance, 1)
+ go func() {
+ defer close(ch)
+
+ subscribeParam := &vo.SubscribeParam{
+ ServiceName: serviceName,
+ SubscribeCallback: func(services []model.Instance, err error) {
+ if err != nil {
+ d.outLog(OutputLogTypeError, fmt.Sprintf("[NacosDiscovery] Subscribe callback error: %v", err))
+ go d.recoverSubscribe(ctx, serviceName, ch)
+ return
+ }
+ instances := make([]instance.ServiceInstance, len(services))
+ for i, svc := range services {
+ httpPort := strconv.FormatUint(svc.Port, 10)
+ grpcPort := ""
+ if port, ok := svc.Metadata["http_port"]; ok {
+ httpPort = port
+ }
+ if port, ok := svc.Metadata["grpc_port"]; ok {
+ grpcPort = port
+ }
+
+ instances[i] = instance.ServiceInstance{
+ InstanceID: svc.InstanceId,
+ Host: svc.Ip,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ Metadata: svc.Metadata,
+ }
+ }
+
+ select {
+ case ch <- instances:
+ case <-ctx.Done():
+ return
+ }
+ },
+ }
+
+ cli := d.pool.Get()
+ operation := func() error {
+ subscribeErr := cli.Subscribe(subscribeParam)
+ return subscribeErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ d.outLog(OutputLogTypeError, fmt.Sprintf("[NacosDiscovery] Failed to subscribe: %v", err))
+ d.pool.Put(cli)
+ return
+ }
+ d.pool.Put(cli)
+
+ <-ctx.Done()
+ _ = cli.Unsubscribe(subscribeParam)
+ }()
+ return ch, nil
+}
+
+// recoverSubscribe try to restore the subscription
+func (d *NacosDiscovery) recoverSubscribe(ctx context.Context, serviceName string, ch chan []instance.ServiceInstance) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ subscribeParam := &vo.SubscribeParam{
+ ServiceName: serviceName,
+ SubscribeCallback: func(services []model.Instance, err error) {
+ if err != nil {
+ d.outLog(OutputLogTypeError, fmt.Sprintf("[NacosDiscovery] Subscribe to NACOS callback error: %v", err))
+ return
+ }
+ instances := make([]instance.ServiceInstance, len(services))
+ for i, svc := range services {
+ httpPort := strconv.FormatUint(svc.Port, 10)
+ grpcPort := ""
+ if port, ok := svc.Metadata["http_port"]; ok {
+ httpPort = port
+ }
+ if port, ok := svc.Metadata["grpc_port"]; ok {
+ grpcPort = port
+ }
+
+ instances[i] = instance.ServiceInstance{
+ InstanceID: svc.InstanceId,
+ Host: svc.Ip,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ Metadata: svc.Metadata,
+ }
+ }
+
+ select {
+ case ch <- instances:
+ case <-ctx.Done():
+ return
+ }
+ },
+ }
+
+ operation := func() error {
+ subscribeErr := cli.Subscribe(subscribeParam)
+ return subscribeErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[NacosDiscovery] The NACOS subscription cannot be restored: %v", err))
+ return
+ }
+
+ d.outLog(OutputLogTypeInfo, "[NacosDiscovery] Successfully restored the NACOS subscription")
+
+ if err := d.checkAndReRegisterServices(ctx); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+}
+
+// GetInstances .
+func (d *NacosDiscovery) GetInstances(serviceName string) ([]instance.ServiceInstance, error) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ var service model.Service
+
+ operation := func() error {
+ var getErr error
+ service, getErr = cli.GetService(vo.GetServiceParam{
+ ServiceName: serviceName,
+ })
+ return getErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ if err = d.checkAndReRegisterServices(context.Background()); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+ return nil, fmt.Errorf("failed to get instances: %v", err)
+ }
+
+ result := make([]instance.ServiceInstance, len(service.Hosts))
+ for i, inst := range service.Hosts {
+ httpPort := strconv.FormatUint(inst.Port, 10)
+ grpcPort := ""
+ if port, ok := inst.Metadata["http_port"]; ok {
+ httpPort = port
+ }
+ if port, ok := inst.Metadata["grpc_port"]; ok {
+ grpcPort = port
+ }
+
+ result[i] = instance.ServiceInstance{
+ InstanceID: inst.InstanceId,
+ Host: inst.Ip,
+ Metadata: inst.Metadata,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ }
+ }
+ return result, nil
+}
+
+// Close .
+func (d *NacosDiscovery) Close() error {
+ d.pool.Close()
+ return nil
+}
diff --git a/servicediscovery/noop_discovery.go b/servicediscovery/noop_discovery.go
new file mode 100644
index 0000000..cb3463a
--- /dev/null
+++ b/servicediscovery/noop_discovery.go
@@ -0,0 +1,47 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package servicediscovery
+
+import (
+ "context"
+
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// NoopDiscovery ..
+type NoopDiscovery struct{}
+
+// SetOutputLogCallback .
+func (n *NoopDiscovery) SetOutputLogCallback(outputLogCallback OutputLogCallback) {
+}
+
+// Register .
+func (n *NoopDiscovery) Register(ctx context.Context, serviceName, instanceID, addr, httpPort, grpcPort string) error {
+ return nil
+}
+
+// Deregister .
+func (n *NoopDiscovery) Deregister(ctx context.Context, serviceName, instanceID string) error {
+ return nil
+}
+
+// Watch .
+func (n *NoopDiscovery) Watch(ctx context.Context, serviceName string) (chan []instance.ServiceInstance, error) {
+ ch := make(chan []instance.ServiceInstance)
+ close(ch)
+ return ch, nil
+}
+
+// GetInstances .
+func (n *NoopDiscovery) GetInstances(serviceName string) ([]instance.ServiceInstance, error) {
+ return []instance.ServiceInstance{}, nil
+}
+
+// Close .
+func (n *NoopDiscovery) Close() error {
+ return nil
+}
diff --git a/servicediscovery/zookeeper_discovery.go b/servicediscovery/zookeeper_discovery.go
new file mode 100644
index 0000000..40c240c
--- /dev/null
+++ b/servicediscovery/zookeeper_discovery.go
@@ -0,0 +1,417 @@
+/**
+ * @Author Awen
+ * @Date 2025/06/18
+ * @Email wengaolng@gmail.com
+ **/
+
+package servicediscovery
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "path"
+ "sync"
+ "time"
+
+ "github.com/go-zookeeper/zk"
+ "github.com/google/uuid"
+ "github.com/wenlng/go-service-link/foundation/clientpool"
+ "github.com/wenlng/go-service-link/foundation/common"
+ "github.com/wenlng/go-service-link/foundation/extraconfig"
+ "github.com/wenlng/go-service-link/foundation/helper"
+ "github.com/wenlng/go-service-link/servicediscovery/instance"
+)
+
+// ZooKeeperDiscovery .
+type ZooKeeperDiscovery struct {
+ instanceID string
+ pool *clientpool.ZooKeeperPool
+ outputLogCallback OutputLogCallback
+ config ZooKeeperDiscoveryConfig
+
+ registeredServices map[string]registeredServiceInfo
+ mutex sync.RWMutex
+}
+
+// ZooKeeperDiscoveryConfig .
+type ZooKeeperDiscoveryConfig struct {
+ extraconfig.ZooKeeperExtraConfig
+
+ address []string
+ poolSize int
+ ttl time.Duration
+ keepAlive time.Duration
+ maxRetries int
+ baseRetryDelay time.Duration
+ tlsConfig *common.TLSConfig
+ username string
+ password string
+}
+
+// NewZooKeeperDiscovery .
+func NewZooKeeperDiscovery(config ZooKeeperDiscoveryConfig) (*ZooKeeperDiscovery, error) {
+ if config.poolSize <= 0 {
+ config.poolSize = 5
+ }
+
+ if config.maxRetries <= 0 {
+ config.maxRetries = 3
+ }
+
+ if !helper.IsDurationSet(config.baseRetryDelay) {
+ config.baseRetryDelay = 500 * time.Millisecond
+ }
+
+ config.ZooKeeperExtraConfig.Username = config.username
+ config.ZooKeeperExtraConfig.Password = config.password
+
+ zd := &ZooKeeperDiscovery{
+ config: config,
+ registeredServices: make(map[string]registeredServiceInfo),
+ }
+
+ zlogger := &extraconfig.Zlogger{
+ OutLogCallback: func(format string, s ...interface{}) {
+ if zd.outputLogCallback != nil {
+ zd.outputLogCallback(OutputLogTypeInfo, fmt.Sprintf(format, s...))
+ }
+ },
+ }
+
+ excfg := &config.ZooKeeperExtraConfig
+ excfg.SetZlogger(zlogger)
+
+ pool, err := clientpool.NewZooKeeperPool(config.poolSize, config.address, excfg)
+ if err != nil {
+ return nil, err
+ }
+
+ zd.pool = pool
+ return zd, nil
+}
+
+// SetOutputLogCallback .
+func (d *ZooKeeperDiscovery) SetOutputLogCallback(outputLogCallback OutputLogCallback) {
+ d.outputLogCallback = outputLogCallback
+}
+
+// outLog
+func (d *ZooKeeperDiscovery) outLog(logType OutputLogType, message string) {
+ if d.outputLogCallback != nil {
+ d.outputLogCallback(logType, message)
+ }
+}
+
+// checkAndReRegisterServices check and re-register the service
+func (d *ZooKeeperDiscovery) checkAndReRegisterServices(ctx context.Context) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ d.mutex.RLock()
+ services := make(map[string]registeredServiceInfo)
+ for k, v := range d.registeredServices {
+ services[k] = v
+ }
+ d.mutex.RUnlock()
+
+ for instanceID, svcInfo := range services {
+ p := path.Join("/services", svcInfo.ServiceName, instanceID)
+ operation := func() error {
+ exists, _, err := cli.Exists(p)
+ if err != nil {
+ return fmt.Errorf("failed to check the service registration status: %v", err)
+ }
+ if !exists {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperDiscovery] The service has not been registered. Re-register: %s, instanceID: %s", svcInfo.ServiceName, instanceID))
+ return d.register(ctx, svcInfo.ServiceName, instanceID, svcInfo.Host, svcInfo.HTTPPort, svcInfo.GRPCPort, true)
+ }
+ return nil
+ }
+
+ if err := helper.WithRetry(ctx, operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperDiscovery] The re-registration service failed: %v", err))
+ return err
+ }
+ }
+ return nil
+}
+
+// register .
+func (d *ZooKeeperDiscovery) register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string, isReRegister bool) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ if instanceID == "" {
+ instanceID = uuid.New().String()
+ }
+ d.instanceID = instanceID
+
+ data, err := json.Marshal(instance.ServiceInstance{
+ InstanceID: instanceID,
+ Host: host,
+ Metadata: map[string]string{
+ "hostname": helper.GetHostname(),
+ "host": host,
+ "http_port": httpPort,
+ "grpc_port": grpcPort,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("failed to marshal instance: %v", err)
+ }
+
+ p := path.Join("/services", serviceName, instanceID)
+
+ if !isReRegister {
+ d.mutex.Lock()
+ d.registeredServices[instanceID] = registeredServiceInfo{
+ ServiceName: serviceName,
+ InstanceID: instanceID,
+ Host: host,
+ HTTPPort: httpPort,
+ GRPCPort: grpcPort,
+ }
+ d.mutex.Unlock()
+
+ go d.keepAliveLoop(ctx, p, data)
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[ZookeeperDiscovery] The registration service is beginning...., service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+
+ err = d.ensureParentNodes(p)
+ if err != nil {
+ return err
+ }
+
+ operation := func() error {
+ _, createErr := cli.Create(p, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
+ if createErr != nil && createErr != zk.ErrNodeExists {
+ return fmt.Errorf("the Zookeeper instance cannot be registered: %v", createErr)
+ }
+ return nil
+ }
+ if err = helper.WithRetry(context.Background(), operation); err != nil {
+ return err
+ }
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[ZooKeeperDiscovery] Registered instance, service: %s, instanceId: %s, host: %s, http_port: %s, grpc_port: %s",
+ serviceName, instanceID, host, httpPort, grpcPort))
+
+ return nil
+}
+
+// Register .
+func (d *ZooKeeperDiscovery) Register(ctx context.Context, serviceName, instanceID, host, httpPort, grpcPort string) error {
+ return d.register(ctx, serviceName, instanceID, host, httpPort, grpcPort, false)
+}
+
+// ensureParentNodes recursively create the parent node
+func (d *ZooKeeperDiscovery) ensureParentNodes(targetPath string) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ parentPath := path.Dir(targetPath)
+ if parentPath == "/" || parentPath == "." {
+ return nil
+ }
+
+ var exists bool
+ operation := func() error {
+ var checkErr error
+ exists, _, checkErr = cli.Exists(parentPath)
+ return checkErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ return fmt.Errorf("failed to check the parent node %s: %v", parentPath, err)
+ }
+
+ if exists {
+ return nil
+ }
+
+ if err := d.ensureParentNodes(parentPath); err != nil {
+ return err
+ }
+
+ operation = func() error {
+ _, createErr := cli.Create(parentPath, []byte{}, 0, zk.WorldACL(zk.PermAll))
+ if createErr != nil && createErr != zk.ErrNodeExists {
+ return fmt.Errorf("the parent node of Zookeeper cannot be created %s: %v", parentPath, createErr)
+ }
+ return nil
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// keepAliveLoop .
+func (d *ZooKeeperDiscovery) keepAliveLoop(ctx context.Context, path string, data []byte) {
+ ticker := time.NewTicker(d.config.keepAlive)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ cli := d.pool.Get()
+ var exists bool
+ operation := func() error {
+ var checkErr error
+ exists, _, checkErr = cli.Exists(path)
+ return checkErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperDiscovery] Failed to check instance: %v", err))
+ if err := d.checkAndReRegisterServices(ctx); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperDiscovery] The re-registration service failed: %v", err))
+ }
+ d.pool.Put(cli)
+ continue
+ }
+
+ if !exists {
+ operation = func() error {
+ _, createErr := cli.Create(path, data, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
+ if createErr != nil && createErr != zk.ErrNodeExists {
+ return fmt.Errorf("the Zoopeeker instance cannot be recreated: %v", createErr)
+ }
+ return nil
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperDiscovery] Failed to check instance: %v", err))
+ }
+ }
+ d.pool.Put(cli)
+ case <-ctx.Done():
+ d.outLog(OutputLogTypeInfo, "Stopping keepalive")
+ return
+ }
+ }
+}
+
+// Deregister .
+func (d *ZooKeeperDiscovery) Deregister(ctx context.Context, serviceName, instanceID string) error {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ p := path.Join("/services", serviceName, instanceID)
+ operation := func() error {
+ deleteErr := cli.Delete(p, -1)
+ if deleteErr != nil && deleteErr != zk.ErrNoNode {
+ return fmt.Errorf("failed to deregister instance: %v", deleteErr)
+ }
+ return nil
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ return err
+ }
+
+ d.mutex.Lock()
+ delete(d.registeredServices, instanceID)
+ d.mutex.Unlock()
+
+ d.outLog(
+ OutputLogTypeInfo,
+ fmt.Sprintf("[ZooKeeperDiscovery] Deregistered instance, service: %s, instanceId: %s", serviceName, instanceID))
+
+ return nil
+}
+
+// Watch .
+func (d *ZooKeeperDiscovery) Watch(ctx context.Context, serviceName string) (chan []instance.ServiceInstance, error) {
+ prefix := path.Join("/services", serviceName)
+ ch := make(chan []instance.ServiceInstance, 1)
+ go func() {
+ defer close(ch)
+ for {
+ instances, err := d.GetInstances(serviceName)
+ if err != nil {
+ d.outLog(OutputLogTypeError, fmt.Sprintf("[ZooKeeperDiscovery] Failed to get instances: %v", err))
+ time.Sleep(time.Second)
+ continue
+ }
+ select {
+ case ch <- instances:
+ case <-ctx.Done():
+ return
+ }
+ cli := d.pool.Get()
+ _, _, wch, err := cli.ChildrenW(prefix)
+ d.pool.Put(cli)
+ if err != nil {
+ d.outLog(OutputLogTypeError, fmt.Sprintf("[ZooKeeperDiscovery] Failed to watch children: %v", err))
+ time.Sleep(time.Second)
+ continue
+ }
+ select {
+ case <-wch:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ return ch, nil
+}
+
+// GetInstances .
+func (d *ZooKeeperDiscovery) GetInstances(serviceName string) ([]instance.ServiceInstance, error) {
+ cli := d.pool.Get()
+ defer d.pool.Put(cli)
+
+ prefix := path.Join("/services", serviceName)
+
+ var children []string
+ operation := func() error {
+ var getErr error
+ children, _, getErr = cli.Children(prefix)
+ return getErr
+ }
+ if err := helper.WithRetry(context.Background(), operation); err != nil {
+ return nil, fmt.Errorf("failed to get instances: %v", err)
+ }
+
+ var instances []instance.ServiceInstance
+ for _, child := range children {
+ data, _, err := cli.Get(path.Join(prefix, child))
+ if err != nil {
+ if err = d.checkAndReRegisterServices(context.Background()); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("The re-registration service failed: %v", err))
+ }
+
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperDiscovery] Failed to get instance data: %v", err))
+ continue
+ }
+ var inst instance.ServiceInstance
+ if err = json.Unmarshal(data, &inst); err != nil {
+ d.outLog(OutputLogTypeWarn, fmt.Sprintf("[ZooKeeperDiscovery] Failed to unmarshal instance: %v", err))
+ continue
+ }
+
+ httpPort := ""
+ grpcPort := ""
+ if port, ok := inst.Metadata["http_port"]; ok {
+ httpPort = port
+ }
+ if port, ok := inst.Metadata["grpc_port"]; ok {
+ grpcPort = port
+ }
+
+ inst.HTTPPort = httpPort
+ inst.GRPCPort = grpcPort
+ instances = append(instances, inst)
+ }
+ return instances, nil
+}
+
+// Close .
+func (d *ZooKeeperDiscovery) Close() error {
+ d.pool.Close()
+ return nil
+}