Prechádzať zdrojové kódy

feat: initial mbsd system config client SDK

Provide Init/Get/Refresh with TTL cache, background refresh, and
safe fallbacks when mbsd is unavailable.

Co-authored-by: Cursor <cursoragent@cursor.com>
郭铭泽 6 dní pred
commit
7ba77e063d
5 zmenil súbory, kde vykonal 527 pridanie a 0 odobranie
  1. 7 0
      .gitignore
  2. 61 0
      README.md
  3. 291 0
      client.go
  4. 43 0
      go.mod
  5. 125 0
      go.sum

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+# Binaries
+*.exe
+*.test
+
+# Go workspace
+go.work
+go.work.sum

+ 61 - 0
README.md

@@ -0,0 +1,61 @@
+# mbsd-sdk
+
+从 **SystemData(mbsd)** 读取 `system_configs` 的 Go 客户端,供 HCS、schs、ngtm 等业务服务引用。
+
+## 模块路径
+
+```text
+git.sxidc.com/health-checkup-system/mbsd-sdk
+```
+
+## 依赖
+
+- `git.sxidc.com/service-supports/dapr_api` — HTTP 调用 mbsd
+- `git.sxidc.com/service-supports/fslog` — 刷新失败告警日志
+
+## 使用示例
+
+```go
+import mbsdsdk "git.sxidc.com/health-checkup-system/mbsd-sdk"
+
+func main() {
+    mbsdsdk.Init(cfg.Services.SDUrl, time.Duration(cfg.Services.TimeoutSec)*time.Second)
+    defer mbsdsdk.Destroy()
+
+    keys := []string{"platform.identity.system_tenant_id", "hcs.activity_blacklist.overdue_threshold"}
+    mbsdsdk.StartBackgroundRefresh(mbsdsdk.CacheTTL, keys...)
+
+    tenantID := mbsdsdk.GetString("platform.identity.system_tenant_id", "默认租户ID")
+}
+```
+
+## 行为说明
+
+| 场景 | 行为 |
+|------|------|
+| `sd_url` 未配置 | 不初始化客户端,`Get*` 返回调用方默认值 |
+| mbsd 不可用 | 使用未过期缓存;否则默认值 |
+| 改配置 | 默认约 60s 内通过后台刷新生效(`CacheTTL`) |
+
+各服务在自身仓库维护 **KnownKeys** 与业务默认值封装(如 HCS 的 `application/support/system_config/keys.go`),不要写进本 SDK。
+
+## 版本发布(Gogs)
+
+```bash
+git tag v0.1.0
+git push origin v0.1.0
+```
+
+业务服务:
+
+```bash
+go get git.sxidc.com/health-checkup-system/mbsd-sdk@v0.1.0
+```
+
+私服需配置 `GOPRIVATE=git.sxidc.com`。
+
+## 本地联调(monorepo)
+
+```go
+replace git.sxidc.com/health-checkup-system/mbsd-sdk => ../mbsd-sdk
+```

+ 291 - 0
client.go

@@ -0,0 +1,291 @@
+// Package mbsdsdk 从 SystemData(mbsd)拉取 system_configs 的客户端 SDK。
+package mbsdsdk
+
+import (
+	"encoding/json"
+	"errors"
+	"math/rand"
+	"net/url"
+	"strconv"
+	"sync"
+	"time"
+
+	"git.sxidc.com/service-supports/dapr_api/invoke"
+	"git.sxidc.com/service-supports/fslog"
+)
+
+const (
+	// DefaultURLPrefix mbsd 配置中心 API 前缀(相对 sd_url/baseUrl)。
+	DefaultURLPrefix = "mbsd/api/v1/systemConfig"
+
+	// CacheTTL 默认缓存有效期;过期后重新请求 mbsd。
+	CacheTTL = 60 * time.Second
+)
+
+// ConfigInfo 与 mbsd.system_configs 返回字段一致。
+type ConfigInfo struct {
+	ID          string `json:"id"`
+	ConfigKey   string `json:"configKey"`
+	ConfigValue string `json:"configValue"`
+	ValueType   string `json:"valueType"`
+	ConfigGroup string `json:"configGroup"`
+	Service     string `json:"service"`
+	Description string `json:"description"`
+}
+
+type cacheEntry struct {
+	info     ConfigInfo
+	loadedAt time.Time
+}
+
+type getByKeysRequest struct {
+	Keys []string `json:"keys"`
+}
+
+type getByKeysResult struct {
+	Infos      []ConfigInfo `json:"infos"`
+	TotalCount int64        `json:"totalCount"`
+}
+
+type msgResponse struct {
+	Success bool   `json:"success"`
+	ErrCode int    `json:"errCode"`
+	Msg     string `json:"msg"`
+}
+
+type infoResponse struct {
+	msgResponse
+	Info getByKeysResult `json:"info"`
+}
+
+var (
+	invokeAPI    *invoke.API
+	urlPrefix    = DefaultURLPrefix
+	cacheTTL     = CacheTTL
+	cache        sync.Map
+	refreshStop  chan struct{}
+	rng          = rand.New(rand.NewSource(time.Now().UnixNano()))
+)
+
+// SetURLPrefix 在 Init 之前调用可覆盖默认 API 前缀。
+func SetURLPrefix(prefix string) {
+	if prefix != "" {
+		urlPrefix = prefix
+	}
+}
+
+// SetCacheTTL 覆盖默认缓存 TTL(供测试或特殊服务使用)。
+func SetCacheTTL(ttl time.Duration) {
+	if ttl > 0 {
+		cacheTTL = ttl
+	}
+}
+
+func storeCache(key string, info ConfigInfo) {
+	cache.Store(key, cacheEntry{info: info, loadedAt: time.Now()})
+}
+
+func loadCache(key string) (cacheEntry, bool) {
+	raw, ok := cache.Load(key)
+	if !ok {
+		return cacheEntry{}, false
+	}
+	return raw.(cacheEntry), true
+}
+
+// Init 初始化 mbsd HTTP 客户端;baseUrl 为空则跳过,读配置一律走调用方默认值。
+func Init(baseUrl string, timeout time.Duration) {
+	if baseUrl == "" || invokeAPI != nil {
+		return
+	}
+	invokeAPI = invoke.NewAPI(baseUrl, timeout)
+}
+
+// Destroy 释放客户端并停止后台刷新。
+func Destroy() {
+	StopBackgroundRefresh()
+	if invokeAPI == nil {
+		return
+	}
+	invoke.DestroyAPI(invokeAPI)
+	invokeAPI = nil
+	cache = sync.Map{}
+}
+
+// StartBackgroundRefresh 定时刷新 keys;失败保留旧缓存。启动前增加随机 jitter 打散多实例齐刷。
+func StartBackgroundRefresh(interval time.Duration, keys ...string) {
+	if invokeAPI == nil || len(keys) == 0 {
+		return
+	}
+	if interval <= 0 {
+		interval = cacheTTL
+	}
+	StopBackgroundRefresh()
+	refreshStop = make(chan struct{})
+	keySlice := append([]string(nil), keys...)
+
+	go func() {
+		jitter := time.Duration(rng.Int63n(int64(interval)))
+		timer := time.NewTimer(jitter)
+		select {
+		case <-timer.C:
+		case <-refreshStop:
+			timer.Stop()
+			return
+		}
+
+		if err := Refresh(keySlice...); err != nil {
+			fslog.Warn("mbsdsdk: 首次刷新失败,将使用代码兜底直至拉取成功: " + err.Error())
+		}
+
+		ticker := time.NewTicker(interval)
+		defer ticker.Stop()
+		for {
+			select {
+			case <-ticker.C:
+				if err := Refresh(keySlice...); err != nil {
+					fslog.Warn("mbsdsdk: 定时刷新失败,继续使用上一版缓存或兜底: " + err.Error())
+				}
+			case <-refreshStop:
+				return
+			}
+		}
+	}()
+}
+
+// StopBackgroundRefresh 停止后台刷新协程。
+func StopBackgroundRefresh() {
+	if refreshStop != nil {
+		close(refreshStop)
+		refreshStop = nil
+	}
+}
+
+// Refresh 立即从 mbsd 拉取并更新缓存。
+func Refresh(keys ...string) error {
+	configs, err := GetByKeys(keys)
+	if err != nil {
+		return err
+	}
+	for key, value := range configs {
+		storeCache(key, value)
+	}
+	return nil
+}
+
+// GetByKeys 批量查询;成功项写入缓存。
+func GetByKeys(keys []string) (map[string]ConfigInfo, error) {
+	if invokeAPI == nil {
+		return map[string]ConfigInfo{}, nil
+	}
+	if len(keys) == 0 {
+		return map[string]ConfigInfo{}, nil
+	}
+
+	body, err := json.Marshal(&getByKeysRequest{Keys: keys})
+	if err != nil {
+		return nil, err
+	}
+
+	path, err := url.JoinPath(urlPrefix, "getByKeys")
+	if err != nil {
+		return nil, err
+	}
+
+	respBytes, err := invokeAPI.PostJSON(path, body, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var resp infoResponse
+	if err = json.Unmarshal(respBytes, &resp); err != nil {
+		return nil, err
+	}
+	if !resp.Success {
+		return nil, errors.New(resp.Msg)
+	}
+
+	result := make(map[string]ConfigInfo, len(resp.Info.Infos))
+	for _, info := range resp.Info.Infos {
+		result[info.ConfigKey] = info
+		storeCache(info.ConfigKey, info)
+	}
+	return result, nil
+}
+
+// Get 读取单个 key(带 TTL 与过期后 stale 回退)。
+func Get(key string) (ConfigInfo, bool, error) {
+	if invokeAPI == nil {
+		return ConfigInfo{}, false, nil
+	}
+
+	var stale *ConfigInfo
+	if entry, ok := loadCache(key); ok {
+		if time.Since(entry.loadedAt) < cacheTTL {
+			return entry.info, true, nil
+		}
+		if entry.info.ConfigValue != "" {
+			stale = &entry.info
+		}
+	}
+
+	configs, err := GetByKeys([]string{key})
+	if err != nil {
+		if stale != nil {
+			return *stale, true, nil
+		}
+		return ConfigInfo{}, false, err
+	}
+	info, ok := configs[key]
+	if !ok && stale != nil {
+		return *stale, true, nil
+	}
+	return info, ok, nil
+}
+
+// GetString 读取字符串配置;未配置/失败/空值时返回 defaultValue。
+func GetString(key, defaultValue string) string {
+	info, ok, err := Get(key)
+	if err != nil || !ok || info.ConfigValue == "" {
+		return defaultValue
+	}
+	return info.ConfigValue
+}
+
+// GetInt 读取整数配置。
+func GetInt(key string, defaultValue int) int {
+	info, ok, err := Get(key)
+	if err != nil || !ok || info.ConfigValue == "" {
+		return defaultValue
+	}
+	intValue, err := strconv.Atoi(info.ConfigValue)
+	if err != nil {
+		return defaultValue
+	}
+	return intValue
+}
+
+// GetBool 读取布尔配置。
+func GetBool(key string, defaultValue bool) bool {
+	value := GetString(key, "")
+	if value == "" {
+		return defaultValue
+	}
+	boolValue, err := strconv.ParseBool(value)
+	if err != nil {
+		return defaultValue
+	}
+	return boolValue
+}
+
+// GetJSON 将配置值反序列化到 target;未找到 key 时返回错误。
+func GetJSON(key string, target any) error {
+	info, ok, err := Get(key)
+	if err != nil {
+		return err
+	}
+	if !ok {
+		return errors.New("system config not found: " + key)
+	}
+	return json.Unmarshal([]byte(info.ConfigValue), target)
+}

+ 43 - 0
go.mod

@@ -0,0 +1,43 @@
+module git.sxidc.com/health-checkup-system/mbsd-sdk
+
+go 1.22
+
+require (
+	git.sxidc.com/service-supports/dapr_api v0.2.0
+	git.sxidc.com/service-supports/fslog v0.5.9
+)
+
+require (
+	github.com/bytedance/sonic v1.10.0 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
+	github.com/chenzhuoyu/iasm v0.9.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/gin-gonic/gin v1.9.1 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.15.3 // indirect
+	github.com/go-resty/resty/v2 v2.7.0 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
+	github.com/kr/pretty v0.3.1 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/satori/go.uuid v1.2.0 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.11 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.25.0 // indirect
+	golang.org/x/arch v0.5.0 // indirect
+	golang.org/x/crypto v0.13.0 // indirect
+	golang.org/x/net v0.15.0 // indirect
+	golang.org/x/sys v0.12.0 // indirect
+	golang.org/x/text v0.13.0 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 125 - 0
go.sum

@@ -0,0 +1,125 @@
+git.sxidc.com/service-supports/dapr_api v0.2.0 h1:OFTvpvWeIIHASnwPm+7khb6A5kOmy8dtH5C3CO2J2Kk=
+git.sxidc.com/service-supports/dapr_api v0.2.0/go.mod h1:lvu4Pba0W9ne4vZFs/H5cxhjedD80MxAeLtCw9B8jJs=
+git.sxidc.com/service-supports/fslog v0.5.9 h1:q2XIK2o/fk/qmByy4x5kKLC+k7kolT5LrXHcWRSffXQ=
+git.sxidc.com/service-supports/fslog v0.5.9/go.mod h1:/m03ATmmOle75qtEgvEw8a1+Dcg6iHp08M1bGFXJTBU=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
+github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk=
+github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
+github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
+github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
+github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
+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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo=
+github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
+github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+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/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
+github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
+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.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
+go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
+golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=