提交 105f3c9a authored 作者: mooncake9527's avatar mooncake9527

update

上级 bbc4005a
// Package consulcli is connecting to the consul service client.
package consulcli
import (
"fmt"
"gitlab.wanzhuangkj.com/tush/xpkg/xerrors/xerror"
"github.com/hashicorp/consul/api"
)
// Init connecting to the consul service
// Note: If the WithConfig(*api.Config) parameter is set, the addr parameter is ignored!
func Init(addr string, opts ...Option) (*api.Client, error) {
o := defaultOptions()
o.apply(opts...)
if o.config != nil {
return api.NewClient(o.config)
}
if addr == "" {
return nil, fmt.Errorf("consul address cannot be empty")
}
c, err := api.NewClient(&api.Config{
Address: addr,
Scheme: o.scheme,
WaitTime: o.waitTime,
Datacenter: o.datacenter,
})
if err != nil {
return nil, xerror.New(err.Error())
}
return c, nil
}
package consulcli
import (
"testing"
"time"
"github.com/hashicorp/consul/api"
)
func TestInit(t *testing.T) {
addr := "192.168.3.37:8500"
cli, err := Init(addr,
WithScheme("http"),
WithWaitTime(time.Second*2),
WithDatacenter(""),
WithToken("your-token"),
)
t.Log(err, cli)
cli, err = Init("", WithConfig(&api.Config{
Address: addr,
Scheme: "http",
WaitTime: time.Second * 2,
Datacenter: "",
}))
t.Log(err, cli)
}
package consulcli
import (
"time"
"github.com/hashicorp/consul/api"
)
// Option set the consul client options.
type Option func(*options)
type options struct {
scheme string
waitTime time.Duration
datacenter string
token string
// if you set this parameter, all fields above are invalid
config *api.Config
}
func defaultOptions() *options {
return &options{
scheme: "http",
waitTime: time.Second * 5,
}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithWaitTime set wait time
func WithWaitTime(waitTime time.Duration) Option {
return func(o *options) {
o.waitTime = waitTime
}
}
// WithScheme set scheme
func WithScheme(scheme string) Option {
return func(o *options) {
o.scheme = scheme
}
}
// WithDatacenter set datacenter
func WithDatacenter(datacenter string) Option {
return func(o *options) {
o.datacenter = datacenter
}
}
// WithToken set token
func WithToken(token string) Option {
return func(o *options) {
o.token = token
}
}
// WithConfig set consul config
func WithConfig(c *api.Config) Option {
return func(o *options) {
o.config = c
}
}
// Package dlock provides distributed locking primitives, supports redis and etcd.
package dlock
import "context"
// Locker is the interface that wraps the basic locking operations.
type Locker interface {
Lock(ctx context.Context) error
Unlock(ctx context.Context) error
TryLock(ctx context.Context) (bool, error)
Close() error
}
package dlock
import (
"context"
"errors"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
)
var defaultTTL = 15 // seconds
type EtcdLock struct {
session *concurrency.Session
mutex *concurrency.Mutex
}
// NewEtcd creates a new etcd locker with the given key and ttl.
func NewEtcd(client *clientv3.Client, key string, ttl int) (Locker, error) {
if client == nil {
return nil, errors.New("etcd client is nil")
}
if key == "" {
return nil, errors.New("key is empty")
}
if ttl <= 0 {
ttl = defaultTTL
}
expiration := time.Duration(ttl) * time.Second
ctx, _ := context.WithTimeout(context.Background(), expiration) //nolint
session, err := concurrency.NewSession(
client,
concurrency.WithTTL(ttl),
concurrency.WithContext(ctx),
)
if err != nil {
return nil, err
}
mutex := concurrency.NewMutex(session, key)
locker := &EtcdLock{
session: session,
mutex: mutex,
}
return locker, nil
}
// Lock blocks until the lock is acquired or the context is canceled.
func (l *EtcdLock) Lock(ctx context.Context) error {
return l.mutex.Lock(ctx)
}
// Unlock releases the lock.
func (l *EtcdLock) Unlock(ctx context.Context) error {
return l.mutex.Unlock(ctx)
}
// TryLock tries to acquire the lock without blocking.
func (l *EtcdLock) TryLock(ctx context.Context) (bool, error) {
err := l.mutex.TryLock(ctx)
if err == nil {
return true, nil
}
if err == concurrency.ErrLocked {
return false, nil
}
return false, err
}
// Close releases the lock and the etcd session.
func (l *EtcdLock) Close() error {
if l.session != nil {
return l.session.Close()
}
return nil
}
package dlock
import (
"fmt"
"testing"
"time"
"gitlab.wanzhuangkj.com/tush/xpkg/etcdcli"
"go.uber.org/zap"
)
func TestEtcdLock_TryLock(t *testing.T) {
initLocker := func() Locker {
return getEtcdLock()
}
testLockAndUnlock(initLocker, false, t)
}
func TestEtcdLock_Lock(t *testing.T) {
initLocker := func() Locker {
return getEtcdLock()
}
testLockAndUnlock(initLocker, true, t)
}
func getEtcdLock() Locker {
endpoints := []string{"127.0.0.1:2379"}
cli, err := etcdcli.Init(endpoints,
etcdcli.WithDialTimeout(time.Second*2),
etcdcli.WithAuth("", ""),
etcdcli.WithAutoSyncInterval(0),
etcdcli.WithLog(zap.NewNop()),
)
if err != nil {
fmt.Println(err)
return nil
}
locker, err := NewEtcd(cli, "xmall/dlock", 10)
if err != nil {
fmt.Println(err)
return nil
}
return locker
}
package dlock
import (
"context"
"errors"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
"github.com/redis/go-redis/v9"
)
// RedisLock implements Locker using Redis.
type RedisLock struct {
mutex *redsync.Mutex
}
// NewRedisLock creates a new RedisLock.
func NewRedisLock(client *redis.Client, key string, options ...redsync.Option) (Locker, error) {
if client == nil {
return nil, errors.New("redis client is nil")
}
if key == "" {
return nil, errors.New("key is empty")
}
return newLocker(client, key, options...), nil
}
// NewRedisClusterLock creates a new RedisClusterLock.
func NewRedisClusterLock(clusterClient *redis.ClusterClient, key string, options ...redsync.Option) (Locker, error) {
if clusterClient == nil {
return nil, errors.New("cluster redis client is nil")
}
if key == "" {
return nil, errors.New("key is empty")
}
return newLocker(clusterClient, key, options...), nil
}
func newLocker(delegate redis.UniversalClient, key string, options ...redsync.Option) Locker {
pool := goredis.NewPool(delegate)
rs := redsync.New(pool)
mutex := rs.NewMutex(key, options...)
return &RedisLock{
mutex: mutex,
}
}
// TryLock tries to acquire the lock without blocking.
func (l *RedisLock) TryLock(ctx context.Context) (bool, error) {
err := l.mutex.TryLockContext(ctx)
if err == nil {
return true, nil
}
return false, err
}
// Lock blocks until the lock is acquired or the context is canceled.
func (l *RedisLock) Lock(ctx context.Context) error {
return l.mutex.LockContext(ctx)
}
// Unlock releases the lock, if unlocking the key is successful, the key will be automatically deleted
func (l *RedisLock) Unlock(ctx context.Context) error {
_, err := l.mutex.UnlockContext(ctx)
return err
}
// Close no-op for RedisLock.
func (l *RedisLock) Close() error {
return nil
}
package dlock
import (
"context"
"fmt"
"sync"
"testing"
"time"
"gitlab.wanzhuangkj.com/tush/xpkg/goredis"
)
func TestRedisLock_TryLock(t *testing.T) {
initLocker := func() Locker {
return getRedisLock()
}
testLockAndUnlock(initLocker, false, t)
}
func TestRedisLock_Lock(t *testing.T) {
initLocker := func() Locker {
return getRedisLock()
}
testLockAndUnlock(initLocker, true, t)
}
func TestClusterRedis_TryLock(t *testing.T) {
initLocker := func() Locker {
return getClusterRedisLock()
}
testLockAndUnlock(initLocker, false, t)
}
func TestClusterRedis_Lock(t *testing.T) {
initLocker := func() Locker {
return getClusterRedisLock()
}
testLockAndUnlock(initLocker, true, t)
}
func getRedisLock() Locker {
redisCli, err := goredis.Init("default:123456@127.0.0.1:6379")
if err != nil {
fmt.Println(err)
return nil
}
locker, err := NewRedisLock(redisCli, "test_lock")
if err != nil {
return nil
}
return locker
}
func getClusterRedisLock() Locker {
addrs := []string{"127.0.0.1:6380", "127.0.0.1:6381", "127.0.0.1:6382"}
clusterClient, err := goredis.InitCluster(addrs, "", "123456")
if err != nil {
fmt.Println(err)
return nil
}
locker, err := NewRedisClusterLock(clusterClient, "test_cluster_lock")
if err != nil {
return nil
}
return locker
}
func testLockAndUnlock(initLocker func() Locker, isBlock bool, t *testing.T) {
waitGroup := &sync.WaitGroup{}
for i := 1; i <= 10; i++ {
waitGroup.Add(1)
go func(i int) {
defer waitGroup.Done()
ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
NO := fmt.Sprintf("[NO-%d] ", i)
locker := initLocker()
if locker == nil {
t.Log("logger init failed")
return
}
var err error
var ok bool
for {
select {
case <-ctx.Done():
return
default:
}
time.Sleep(time.Millisecond * 50)
if isBlock {
err = locker.Lock(ctx)
if err == nil {
ok = true
}
} else {
ok, err = locker.TryLock(ctx)
}
if err != nil {
//t.Log(NO+"try lock error:", err)
continue
}
if ok {
t.Log(NO + "acquire lock success, and do something")
time.Sleep(time.Millisecond * 200)
err = locker.Unlock(ctx)
if err != nil {
return
}
t.Log(NO + "unlock done")
return
}
}
}(i)
}
waitGroup.Wait()
}
// Package etcdcli is use for connecting to the etcd service
package etcdcli
import (
"fmt"
"gitlab.wanzhuangkj.com/tush/xpkg/xerrors/xerror"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
)
// Init connecting to the etcd service
// Note: If the WithConfig(*clientv3.Config) parameter is set, the endpoints parameter is ignored!
func Init(endpoints []string, opts ...Option) (*clientv3.Client, error) {
o := defaultOptions()
o.apply(opts...)
if o.config != nil {
return clientv3.New(*o.config)
}
if len(endpoints) == 0 {
return nil, fmt.Errorf("etcd endpoints cannot be empty")
}
conf := clientv3.Config{
Endpoints: endpoints,
DialTimeout: o.dialTimeout,
DialKeepAliveTime: 20 * time.Second,
DialKeepAliveTimeout: 10 * time.Second,
AutoSyncInterval: o.autoSyncInterval,
Logger: o.logger,
Username: o.username,
Password: o.password,
}
if !o.isSecure {
conf.DialOptions = append(conf.DialOptions, grpc.WithTransportCredentials(insecure.NewCredentials()))
} else {
cred, err := credentials.NewClientTLSFromFile(o.certFile, o.serverNameOverride)
if err != nil {
return nil, fmt.Errorf("NewClientTLSFromFile error: %v", err)
}
conf.DialOptions = append(conf.DialOptions, grpc.WithTransportCredentials(cred))
}
cli, err := clientv3.New(conf)
if err != nil {
return nil, xerror.Errorf("connecting to the etcd service error: %v", err)
}
return cli, nil
}
package etcdcli
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/zap"
)
func TestInit(t *testing.T) {
endpoints := []string{"192.168.3.37:2379"}
cli, err := Init(endpoints,
WithDialTimeout(time.Second*2),
WithAuth("", ""),
WithAutoSyncInterval(0),
WithLog(zap.NewNop()),
)
t.Log(err, cli)
cli, err = Init(nil, WithConfig(&clientv3.Config{
Endpoints: endpoints,
DialTimeout: time.Second * 2,
Username: "",
Password: "",
}))
t.Log(err, cli)
// test error
_, err = Init(endpoints,
WithDialTimeout(time.Second),
WithSecure("foo", "notfound.crt"))
assert.Error(t, err)
endpoints = nil
_, err = Init(endpoints)
assert.Error(t, err)
}
package etcdcli
import (
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/zap"
)
// Option set the etcd client options.
type Option func(*options)
type options struct {
dialTimeout time.Duration // connection timeout, unit(second)
username string
password string
isSecure bool
serverNameOverride string // etcd domain
certFile string // path to certificate file
autoSyncInterval time.Duration // automatic synchronization of member list intervals
logger *zap.Logger
// if you set this parameter, all fields above are invalid
config *clientv3.Config
}
func defaultOptions() *options {
return &options{
dialTimeout: time.Second * 5,
}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithDialTimeout set dial timeout
func WithDialTimeout(duration time.Duration) Option {
return func(o *options) {
o.dialTimeout = duration
}
}
// WithAuth set authentication
func WithAuth(username string, password string) Option {
return func(o *options) {
o.username = username
o.password = password
}
}
// WithSecure set tls
func WithSecure(serverNameOverride string, certFile string) Option {
return func(o *options) {
o.isSecure = true
o.serverNameOverride = serverNameOverride
o.certFile = certFile
}
}
// WithAutoSyncInterval set auto sync interval value
func WithAutoSyncInterval(duration time.Duration) Option {
return func(o *options) {
o.autoSyncInterval = duration
}
}
// WithLog set logger
func WithLog(l *zap.Logger) Option {
return func(o *options) {
o.logger = l
}
}
// WithConfig set etcd client config
func WithConfig(c *clientv3.Config) Option {
return func(o *options) {
o.config = c
}
}
...@@ -6,33 +6,24 @@ toolchain go1.23.9 ...@@ -6,33 +6,24 @@ toolchain go1.23.9
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/IBM/sarama v1.43.2
github.com/alicebob/miniredis/v2 v2.23.0 github.com/alicebob/miniredis/v2 v2.23.0
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6
github.com/bojand/ghz v0.117.0
github.com/dgraph-io/ristretto v0.1.1 github.com/dgraph-io/ristretto v0.1.1
github.com/felixge/fgprof v0.9.3 github.com/felixge/fgprof v0.9.3
github.com/fsnotify/fsnotify v1.5.4 github.com/fsnotify/fsnotify v1.5.4
github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.20.0 github.com/go-playground/validator/v10 v10.20.0
github.com/go-redsync/redsync/v4 v4.12.1 github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-sql-driver/mysql v1.7.0
github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang-jwt/jwt/v5 v5.0.0
github.com/golang/snappy v0.0.4 github.com/golang/snappy v0.0.4
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.1
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/hashicorp/consul/api v1.12.0
github.com/huandu/xstrings v1.4.0 github.com/huandu/xstrings v1.4.0
github.com/jinzhu/copier v0.3.5 github.com/jinzhu/copier v0.3.5
github.com/jinzhu/inflection v1.0.0 github.com/jinzhu/inflection v1.0.0 // indirect
github.com/nacos-group/nacos-sdk-go/v2 v2.2.7
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_golang v1.14.0
github.com/rabbitmq/amqp091-go v1.9.0
github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 github.com/redis/go-redis/extra/redisotel/v9 v9.7.0
github.com/redis/go-redis/v9 v9.7.0 github.com/redis/go-redis/v9 v9.7.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
...@@ -45,11 +36,7 @@ require ( ...@@ -45,11 +36,7 @@ require (
github.com/swaggo/swag v1.8.12 github.com/swaggo/swag v1.8.12
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.2.3 github.com/uptrace/opentelemetry-go-extra/otelgorm v0.2.3
github.com/vmihailenco/msgpack v4.0.4+incompatible github.com/vmihailenco/msgpack v4.0.4+incompatible
github.com/zhufuyi/sqlparser v1.0.0
go.etcd.io/etcd/client/v3 v3.5.13
go.mongodb.org/mongo-driver v1.14.0
go.opentelemetry.io/contrib v1.24.0 go.opentelemetry.io/contrib v1.24.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0
go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 go.opentelemetry.io/otel/exporters/jaeger v1.17.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0
...@@ -60,7 +47,7 @@ require ( ...@@ -60,7 +47,7 @@ require (
golang.org/x/sync v0.12.0 golang.org/x/sync v0.12.0
google.golang.org/grpc v1.67.1 google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.5
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.5.2 gorm.io/driver/mysql v1.5.2
gorm.io/driver/postgres v1.5.4 gorm.io/driver/postgres v1.5.4
gorm.io/driver/sqlite v1.5.4 gorm.io/driver/sqlite v1.5.4
...@@ -78,51 +65,27 @@ require ( ...@@ -78,51 +65,27 @@ require (
require ( require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/kr/pretty v0.3.1 // indirect
go.uber.org/goleak v1.3.0 // indirect
golang.org/x/image v0.23.0 // indirect golang.org/x/image v0.23.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
) )
require ( require (
cel.dev/expr v0.16.0 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
github.com/BurntSushi/toml v1.1.0 // indirect github.com/BurntSushi/toml v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
github.com/alibabacloud-go/tea v1.1.17 // indirect
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.76 // indirect
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2 // indirect
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7 // indirect
github.com/armon/go-metrics v0.3.10 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protocompile v0.4.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/go-resiliency v1.6.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/envoyproxy/go-control-plane v0.13.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
...@@ -135,70 +98,38 @@ require ( ...@@ -135,70 +98,38 @@ require (
github.com/go-playground/locales v0.14.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/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.2.2 // indirect github.com/golang/glog v1.2.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.2.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/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.9.7 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jhump/protoreflect v1.15.1 // indirect
github.com/jinzhu/configor v1.2.1 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.6 // indirect github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/afero v1.10.0 // indirect github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
...@@ -208,26 +139,18 @@ require ( ...@@ -208,26 +139,18 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.3 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/etcd/api/v3 v3.5.13 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/net v0.37.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.1.0 // indirect golang.org/x/time v0.1.0 // indirect
golang.org/x/tools v0.30.0 // indirect golang.org/x/tools v0.30.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
......
cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y=
cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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.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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
...@@ -25,8 +23,6 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf ...@@ -25,8 +23,6 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 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.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/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 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/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.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
...@@ -46,75 +42,36 @@ github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi ...@@ -46,75 +42,36 @@ github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/IBM/sarama v1.43.2 h1:HABeEqRUh32z8yzY2hGB/j8mHSzC/HA9zlEjqFNCzSw=
github.com/IBM/sarama v1.43.2/go.mod h1:Kyo4WkF24Z+1nz7xeVUFWIuKVV8RS3wM8mkvPKMdXFQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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-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-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.17 h1:05R5DnaJXe9sCNIe8KUgWHC/z6w/VZIwczgUwzRnul8=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
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/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.23.0 h1:+lwAJYjvvdIVg6doFHuotFjueJ/7KY10xo/vm3X3Scw= github.com/alicebob/miniredis/v2 v2.23.0 h1:+lwAJYjvvdIVg6doFHuotFjueJ/7KY10xo/vm3X3Scw=
github.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= github.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.76 h1:mg/+23+/gAw6zdxv9I5dPCj666WJPLk8S1nXm0dOumQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.76/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2 h1:rWkH6D2XlXb/Y+tNAQROxBzp3a0p92ni+pXcaHBe/WI=
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.2.2/go.mod h1:GDtq+Kw+v0fO+j5BrrWiUHbBq7L+hfpzpPfXKOZMFE0=
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7 h1:olLiPI2iM8Hqq6vKnSxpM3awCrm9/BeOgHpzQkOYnI4=
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.7/go.mod h1:oDg1j4kFxnhgftaiLJABkGeSvuEvSF5Lo6UmRAMruX4=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 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.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bojand/ghz v0.117.0 h1:dTMxg+tUcLMw8BYi7vQPjXsrM2DJ20ns53hz1am1SbQ=
github.com/bojand/ghz v0.117.0/go.mod h1:MXspmKdJie7NAS0IHzqG9X5h6zO3tIRGQ6Tkt8sAwa4=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
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/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
...@@ -122,8 +79,6 @@ github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1 ...@@ -122,8 +79,6 @@ github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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.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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
...@@ -131,8 +86,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL ...@@ -131,8 +86,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 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/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/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
...@@ -141,12 +94,6 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ ...@@ -141,12 +94,6 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
...@@ -163,31 +110,14 @@ github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROx ...@@ -163,31 +110,14 @@ github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROx
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30=
github.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 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.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/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les=
github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
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 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
...@@ -244,14 +174,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 ...@@ -244,14 +174,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-redsync/redsync/v4 v4.12.1 h1:hCtdZ45DJxMxNdPiby5GlQwOKQmcka2587Y466qPqlA=
github.com/go-redsync/redsync/v4 v4.12.1/go.mod h1:sn72ojgeEhxUuRjrliK0NRrB0Zl6kOZ3BDvNN3P2jAY=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
...@@ -259,11 +181,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me ...@@ -259,11 +181,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 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/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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.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-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
...@@ -281,8 +199,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt ...@@ -281,8 +199,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/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.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.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.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.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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
...@@ -303,10 +219,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek ...@@ -303,10 +219,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
...@@ -341,7 +254,6 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8I ...@@ -341,7 +254,6 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8I
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
...@@ -349,71 +261,17 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ ...@@ -349,71 +261,17 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
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.1/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 v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
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 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
github.com/hashicorp/go-msgpack v0.5.3/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 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
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.2/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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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.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/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
...@@ -422,22 +280,6 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= ...@@ -422,22 +280,6 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
...@@ -445,17 +287,10 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr ...@@ -445,17 +287,10 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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.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.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.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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
...@@ -463,15 +298,9 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm ...@@ -463,15 +298,9 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 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/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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 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/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
...@@ -481,7 +310,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv ...@@ -481,7 +310,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
...@@ -501,17 +329,6 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN ...@@ -501,17 +329,6 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
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.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
...@@ -519,23 +336,8 @@ github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6 ...@@ -519,23 +336,8 @@ github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 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/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/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
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/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 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/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
...@@ -545,47 +347,31 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G ...@@ -545,47 +347,31 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nacos-group/nacos-sdk-go/v2 v2.2.7 h1:wCC1f3/VzIR1WD30YKeJGZAOchYCK/35mLC8qWt6Q6o=
github.com/nacos-group/nacos-sdk-go/v2 v2.2.7/go.mod h1:VYlyDPlQchPC31PmfBustu81vsOkdpCuO5k0dRdQcFc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
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/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 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.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.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.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
...@@ -598,7 +384,6 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T ...@@ -598,7 +384,6 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
...@@ -606,24 +391,17 @@ github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8 ...@@ -606,24 +391,17 @@ github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 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.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.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.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo=
github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0 h1:BIx9TNZH/Jsr4l1i7VVxnV0JPiwYj8qyrHyuL0fGZrk= github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0 h1:BIx9TNZH/Jsr4l1i7VVxnV0JPiwYj8qyrHyuL0fGZrk=
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0/go.mod h1:eTg/YQtGYAZD5r3DlGlJptJ45AHA+/G+2NPn30PKzik= github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0/go.mod h1:eTg/YQtGYAZD5r3DlGlJptJ45AHA+/G+2NPn30PKzik=
github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 h1:bQk8xiVFw+3ln4pfELVktpWgYdFpgLLU+quwSoeIof0= github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 h1:bQk8xiVFw+3ln4pfELVktpWgYdFpgLLU+quwSoeIof0=
github.com/redis/go-redis/extra/redisotel/v9 v9.7.0/go.mod h1:0LyN+GHLIJmKtjYRPF7nHyTTMV6E91YngoOopNifQRo= github.com/redis/go-redis/extra/redisotel/v9 v9.7.0/go.mod h1:0LyN+GHLIJmKtjYRPF7nHyTTMV6E91YngoOopNifQRo=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
...@@ -632,17 +410,12 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po ...@@ -632,17 +410,12 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE= github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE=
github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ= github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 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.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
...@@ -651,7 +424,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 ...@@ -651,7 +424,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
...@@ -679,8 +451,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl ...@@ -679,8 +451,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
...@@ -695,13 +465,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA ...@@ -695,13 +465,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 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/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
...@@ -713,35 +478,16 @@ github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.3/go.mod h1:jyigonKik3C5V ...@@ -713,35 +478,16 @@ github.com/uptrace/opentelemetry-go-extra/otelsql v0.2.3/go.mod h1:jyigonKik3C5V
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/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.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.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zhufuyi/sqlparser v1.0.0 h1:hKYDokSo5joK5i4YqV1oiqWueU/QC+mqvFsBXwsh4G0=
github.com/zhufuyi/sqlparser v1.0.0/go.mod h1:uNtQggAJNXcVriMAqwo4R9zWYAcST+OKbV0ef+UdScU=
go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4=
go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c=
go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg=
go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8=
go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js=
go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 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.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
...@@ -750,8 +496,6 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= ...@@ -750,8 +496,6 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/contrib v1.24.0 h1:Tfn7pP/482iIzeeba91tP52a1c1TEeqYc1saih+vBN8= go.opentelemetry.io/contrib v1.24.0 h1:Tfn7pP/482iIzeeba91tP52a1c1TEeqYc1saih+vBN8=
go.opentelemetry.io/contrib v1.24.0/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co= go.opentelemetry.io/contrib v1.24.0/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
...@@ -766,7 +510,6 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y ...@@ -766,7 +510,6 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
...@@ -780,25 +523,18 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf ...@@ -780,25 +523,18 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 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/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/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-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-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-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
...@@ -808,7 +544,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 ...@@ -808,7 +544,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/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-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-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 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/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
...@@ -856,7 +591,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL ...@@ -856,7 +591,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-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-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-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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
...@@ -876,8 +610,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY ...@@ -876,8 +610,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
...@@ -886,9 +618,7 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su ...@@ -886,9 +618,7 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
...@@ -906,8 +636,6 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ ...@@ -906,8 +636,6 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
...@@ -927,13 +655,11 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= ...@@ -927,13 +655,11 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-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-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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/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-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-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-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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
...@@ -943,18 +669,12 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w ...@@ -943,18 +669,12 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/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-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-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-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-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-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/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-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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
...@@ -977,18 +697,14 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w ...@@ -977,18 +697,14 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/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-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-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-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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
...@@ -996,7 +712,6 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc ...@@ -996,7 +712,6 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
...@@ -1009,7 +724,6 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= ...@@ -1009,7 +724,6 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
...@@ -1024,7 +738,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ...@@ -1024,7 +738,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
...@@ -1038,10 +751,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb ...@@ -1038,10 +751,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/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-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-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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
...@@ -1054,7 +765,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw ...@@ -1054,7 +765,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/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-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-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-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-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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
...@@ -1079,7 +789,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY ...@@ -1079,7 +789,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/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-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-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-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-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-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
...@@ -1088,10 +797,8 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f ...@@ -1088,10 +797,8 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
...@@ -1103,10 +810,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T ...@@ -1103,10 +810,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 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.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.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
...@@ -1171,8 +874,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D ...@@ -1171,8 +874,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
...@@ -1220,7 +921,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV ...@@ -1220,7 +921,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
...@@ -1230,14 +930,12 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= ...@@ -1230,14 +930,12 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/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.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.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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
......
// Package gobash provides the ability to execute commands, scripts, executables in the go environment with live log output.
package gobash
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os/exec"
"strings"
)
// Exec suitable for executing a single non-blocking command, outputting standard and error logs,
// but the log output is not real time, no execution, command name must be in system path,
// Note: If the execution of a command blocks permanently, it can cause a concurrent leak.
func Exec(name string, args ...string) ([]byte, error) {
cmdName, err := exec.LookPath(name) // cmdName is absolute path
if err != nil {
return nil, err
}
cmd := exec.Command(cmdName, args...)
return getResult(cmd)
}
// Result of the execution of the command
type Result struct {
StdOut chan string
Err error // If nil after the command is executed, the command is executed successfully
}
// Run execute the command, no execution, command name must be in system path,
// you can actively end the command, the execution results are returned in real time in Result.StdOut
func Run(ctx context.Context, name string, args ...string) *Result {
result := &Result{StdOut: make(chan string), Err: error(nil)}
go func() {
defer func() { close(result.StdOut) }() // execution complete, channel closed
cmdName, err := exec.LookPath(name) // cmdName is absolute path
if err != nil {
result.Err = err
return
}
cmd := exec.CommandContext(ctx, cmdName, args...)
handleExec(ctx, cmd, result)
}()
return result
}
func handleExec(ctx context.Context, cmd *exec.Cmd, result *Result) {
result.StdOut <- strings.Join(cmd.Args, " ") + "\n"
stdout, stderr, err := getCmdReader(cmd)
if err != nil {
result.Err = err
return
}
reader := bufio.NewReader(stdout)
// reads each line in real time
line := ""
for {
line, err = reader.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) { // determine if it has been read
break
}
result.Err = err
break
}
select {
case result.StdOut <- line:
case <-ctx.Done():
result.Err = fmt.Errorf("%v", ctx.Err())
return
}
}
// capture error logs
bytesErr, err := io.ReadAll(stderr)
if err != nil {
result.Err = err
return
}
err = cmd.Wait()
if err != nil {
if len(bytesErr) != 0 {
result.Err = errors.New(string(bytesErr))
return
}
result.Err = err
}
}
func getCmdReader(cmd *exec.Cmd) (io.ReadCloser, io.ReadCloser, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, nil, err
}
err = cmd.Start()
if err != nil {
return nil, nil, err
}
return stdout, stderr, nil
}
func getResult(cmd *exec.Cmd) ([]byte, error) {
stdout, stderr, err := getCmdReader(cmd)
if err != nil {
return nil, err
}
bytes, err := io.ReadAll(stdout)
if err != nil {
return nil, err
}
bytesErr, err := io.ReadAll(stderr)
if err != nil {
return nil, err
}
err = cmd.Wait()
if err != nil {
if len(bytesErr) != 0 {
return nil, errors.New(string(bytesErr))
}
return nil, err
}
return bytes, nil
}
package gobash
import (
"context"
"fmt"
"testing"
"time"
)
func TestRun(t *testing.T) {
cmds := map[string][]string{
"pwd": {},
"go": {"env", "GOPATH"},
"bash": {"-c", "for i in $(seq 1 5); do echo 'test cmd' $i;sleep 0.1; done"},
}
for cmd, args := range cmds {
ctx, _ := context.WithTimeout(context.Background(), time.Second)
result := Run(ctx, cmd, args...)
for v := range result.StdOut { // Real-time output of logs and error messages
t.Logf(v)
}
if result.Err != nil {
t.Logf("execute command failed, %v", result.Err)
}
fmt.Println()
}
}
func TestExec(t *testing.T) {
cmds := map[string][]string{
"pwd": {},
"go": {"env", "GOROOT"},
"bash": {"-c", "for i in $(seq 1 5); do echo 'test cmd' $i;sleep 0.1; done"},
}
for cmd, args := range cmds {
out, err := Exec(cmd, args...)
if err != nil {
t.Logf("execute command[%s] failed, %v\n", cmd, err)
continue
}
t.Logf("%s\n", out)
}
}
// Package benchmark is compression testing of rpc methods and generation of reported results.
package benchmark
import (
"fmt"
"os"
"github.com/bojand/ghz/printer"
"github.com/bojand/ghz/runner"
"google.golang.org/protobuf/proto"
)
type Option = runner.Option
// Runner interface
type Runner interface {
Run() error
}
// bench pressing parameters
type bench struct {
rpcServerHost string // rpc server address
protoFile string // proto file
packageName string // proto file package name
serviceName string // proto file service name
methodName string // name of pressure test method
methodRequest proto.Message // input parameters corresponding to the method
dependentProtoFilePath []string // dependent proto file path
total uint // number of requests
options []runner.Option
}
// New create a pressure test instance
//
// invalid parameter total if the option runner.WithRunDuration is set
func New(host string, protoFile string, methodName string, req proto.Message, dependentProtoFilePath []string, total int, options ...runner.Option) (Runner, error) {
data, err := os.ReadFile(protoFile)
if err != nil {
return nil, err
}
packageName := getName(data, packagePattern)
if packageName == "" {
return nil, fmt.Errorf("not found package name in protobuf file %s", protoFile)
}
serviceName := getName(data, servicePattern)
if serviceName == "" {
return nil, fmt.Errorf("not found service name in protobuf file %s", protoFile)
}
methodNames := getMethodNames(data, methodPattern)
mName := matchName(methodNames, methodName)
if mName == "" {
return nil, fmt.Errorf("not found name %s in protobuf file %s", methodName, protoFile)
}
return &bench{
protoFile: protoFile,
packageName: packageName,
serviceName: serviceName,
methodName: mName,
methodRequest: req,
rpcServerHost: host,
total: uint(total),
dependentProtoFilePath: dependentProtoFilePath,
options: options,
}, nil
}
// Run operational performance benchmarking
func (b *bench) Run() error {
callMethod := fmt.Sprintf("%s.%s/%s", b.packageName, b.serviceName, b.methodName)
data, err := proto.Marshal(b.methodRequest)
if err != nil {
return err
}
opts := []runner.Option{
runner.WithTotalRequests(b.total),
runner.WithProtoFile(b.protoFile, b.dependentProtoFilePath),
runner.WithBinaryData(data),
runner.WithInsecure(true),
// more parameter settings https://github.com/bojand/ghz/blob/master/runner/options.go#L41
// example settings: https://github.com/bojand/ghz/blob/master/runner/options_test.go#L79
}
opts = append(opts, b.options...)
report, err := runner.Run(callMethod, b.rpcServerHost, opts...)
if err != nil {
return err
}
return b.saveReport(callMethod, report)
}
func (b *bench) saveReport(callMethod string, report *runner.Report) error {
// specify the output path
outDir := os.TempDir() + string(os.PathSeparator) + "xmall_grpc_benchmark"
_ = os.MkdirAll(outDir, 0777)
outputFile := fmt.Sprintf("%sreport_%s.html", outDir+string(os.PathSeparator), b.methodName)
file, err := os.Create(outputFile)
if err != nil {
return err
}
defer func() {
_ = file.Close()
}()
rp := printer.ReportPrinter{
Out: file,
Report: report,
}
fmt.Printf("\nperformance testing of api '%s' is now complete, copy the report file path to your browser to view,\nreport file: %s\n\n", callMethod, outputFile)
return rp.Print("html")
}
package benchmark
import (
"testing"
"github.com/bojand/ghz/runner"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/types/pluginpb"
)
func TestNew(t *testing.T) {
importProtoFiles := []string{}
_, err := New("localhost", "testProto/test.proto", "Create", nil, importProtoFiles, 100)
assert.NoError(t, err)
_, err = New("localhost", "testProto/test2.proto", "Create", nil, importProtoFiles, 100)
assert.Error(t, err)
_, err = New("localhost", "testProto/test3.proto", "Create", nil, importProtoFiles, 100)
assert.Error(t, err)
_, err = New("localhost", "testProto/test4.proto", "Create", nil, importProtoFiles, 100)
assert.Error(t, err)
}
func Test_params_Run(t *testing.T) {
req := &pluginpb.CodeGeneratorRequest{}
opts := protogen.Options{}
gen, err := opts.New(req)
o1 := gen.Response()
importProtoFiles := []string{}
b, err := New("localhost", "testProto/test.proto", "Create", o1, importProtoFiles, 2)
assert.NoError(t, err)
err = b.Run()
t.Log(err)
}
func Test_bench_saveReport(t *testing.T) {
bc := &bench{methodName: "foo"}
err := bc.saveReport("test", &runner.Report{Name: "foo"})
assert.NoError(t, err)
}
package benchmark
import (
"regexp"
"strings"
)
const (
packagePattern = `\npackage (.*);`
servicePattern = `\nservice (\w+)`
methodPattern = `rpc (\w+)`
)
func getName(data []byte, pattern string) string {
re := regexp.MustCompile(pattern)
matchArr := re.FindStringSubmatch(string(data))
if len(matchArr) == 2 {
return strings.ReplaceAll(matchArr[1], " ", "")
}
return ""
}
func getMethodNames(data []byte, methodPattern string) []string {
re := regexp.MustCompile(methodPattern)
matchArr := re.FindAllStringSubmatch(string(data), -1)
names := []string{}
for _, arr := range matchArr {
if len(arr) == 2 {
names = append(names, strings.ReplaceAll(arr[1], " ", ""))
}
}
return names
}
// match name, not case-sensitive
func matchName(names []string, name string) string {
out := ""
for _, s := range names {
if strings.EqualFold(s, name) {
out = s
break
}
}
return out
}
package benchmark
import (
"testing"
"github.com/stretchr/testify/assert"
)
var testData = []byte(`
syntax = "proto3";
package api.use.v1;
option go_package = "./v1;v1";
service useService {
rpc Create(CreateUseRequest) returns (CreateUseReply) {}
rpc DeleteByID(DeleteUseByIDRequest) returns (DeleteUseByIDReply) {}
}
`)
func Test_getName(t *testing.T) {
actual := getName(testData, packagePattern)
assert.Equal(t, "api.use.v1", actual)
actual = getName(testData, servicePattern)
assert.Equal(t, "useService", actual)
}
func Test_getMethodNames(t *testing.T) {
actual := getMethodNames(testData, methodPattern)
assert.EqualValues(t, []string{"Create", "DeleteByID"}, actual)
}
func Test_matchName(t *testing.T) {
methodNames := []string{"Create", "DeleteByID"}
actual := matchName(methodNames, "Create")
assert.NotEmpty(t, actual)
actual = matchName(methodNames, "a")
assert.Empty(t, actual)
}
syntax = "proto3";
package v1;
option go_package = "./v1;v1";
service userService {
rpc Create(CreateUserRequest) returns (CreateUserReply) {}
rpc DeleteByID(DeleteUserByIDRequest) returns (DeleteUserByIDReply) {}
}
message CreateUserRequest {
string name = 1;
}
message CreateUserReply {
uint64 id = 1;
}
message DeleteUserByIDRequest {
uint64 id = 1;
}
message DeleteUserByIDReply {
}
syntax = "proto3";
// no package error
option go_package = "./v1;v1";
service userService {
rpc Create(CreateUserRequest) returns (CreateUserReply) {}
rpc DeleteByID(DeleteUserByIDRequest) returns (DeleteUserByIDReply) {}
}
message CreateUserRequest {
string name = 1;
}
message CreateUserReply {
uint64 id = 1;
}
message DeleteUserByIDRequest {
uint64 id = 1;
}
message DeleteUserByIDReply {
}
syntax = "proto3";
package v1;
option go_package = "./v1;v1";
// no service error
syntax = "proto3";
package v1;
option go_package = "./v1;v1";
service userService {
// no method error
}
// Package client is generic grpc client-side.
package client
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
)
// Option client option func
type Option func(*options)
type options struct {
builders []resolver.Builder
isLoadBalance bool
credentials credentials.TransportCredentials
unaryInterceptors []grpc.UnaryClientInterceptor
streamInterceptors []grpc.StreamClientInterceptor
dialOptions []grpc.DialOption
}
func defaultOptions() *options {
return &options{}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithServiceDiscover set service discover
func WithServiceDiscover(builders ...resolver.Builder) Option {
return func(o *options) {
o.builders = builders
}
}
// WithLoadBalance set load balance
func WithLoadBalance() Option {
return func(o *options) {
o.isLoadBalance = true
}
}
// WithSecure set secure
func WithSecure(credential credentials.TransportCredentials) Option {
return func(o *options) {
o.credentials = credential
}
}
// WithUnaryInterceptor set unary interceptor
func WithUnaryInterceptor(interceptors ...grpc.UnaryClientInterceptor) Option {
return func(o *options) {
o.unaryInterceptors = interceptors
}
}
// WithStreamInterceptor set stream interceptor
func WithStreamInterceptor(interceptors ...grpc.StreamClientInterceptor) Option {
return func(o *options) {
o.streamInterceptors = interceptors
}
}
// WithDialOption set DialOption
func WithDialOption(dialOptions ...grpc.DialOption) Option {
return func(o *options) {
o.dialOptions = dialOptions
}
}
// NewClient create a new grpc client
func NewClient(endpoint string, opts ...Option) (*grpc.ClientConn, error) {
o := defaultOptions()
o.apply(opts...)
var dialOptions []grpc.DialOption
// service discovery
if len(o.builders) > 0 {
dialOptions = append(dialOptions, grpc.WithResolvers(o.builders...))
}
// load balance option
if o.isLoadBalance {
dialOptions = append(dialOptions, grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`))
}
// secure option
if o.credentials == nil {
dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials()))
} else {
dialOptions = append(dialOptions, grpc.WithTransportCredentials(o.credentials))
}
// custom dial option
if len(o.dialOptions) > 0 {
dialOptions = append(dialOptions, o.dialOptions...)
}
// custom unary interceptor option
if len(o.unaryInterceptors) > 0 {
dialOptions = append(dialOptions, grpc.WithChainUnaryInterceptor(o.unaryInterceptors...))
}
// custom stream interceptor option
if len(o.streamInterceptors) > 0 {
dialOptions = append(dialOptions, grpc.WithChainStreamInterceptor(o.streamInterceptors...))
}
return grpc.NewClient(endpoint, dialOptions...)
}
// Dial to grpc server
func Dial(_ context.Context, endpoint string, opts ...Option) (*grpc.ClientConn, error) {
return NewClient(endpoint, opts...)
}
package client
import (
"context"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
)
type builder struct{}
func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
return nil, nil
}
func (b *builder) Scheme() string {
return ""
}
var unaryInterceptors = []grpc.UnaryClientInterceptor{
func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return nil
},
}
var streamInterceptors = []grpc.StreamClientInterceptor{
func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
},
}
func TestNewClient(t *testing.T) {
conn, err := NewClient("127.0.0.1:50082",
WithServiceDiscover(new(builder)),
WithLoadBalance(),
WithSecure(insecure.NewCredentials()),
WithUnaryInterceptor(unaryInterceptors...),
WithStreamInterceptor(streamInterceptors...),
WithDialOption(grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`)),
)
defer conn.Close()
t.Log(conn, err)
time.Sleep(time.Second)
}
// Package grpccli is grpc client with support for service discovery, logging, load balancing, trace, metrics, retries, circuit breaker.
package grpccli
import (
"context"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/gtls"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/interceptor"
"gitlab.wanzhuangkj.com/tush/xpkg/logger"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/discovery"
)
// NewClient creates a new grpc client
func NewClient(endpoint string, opts ...Option) (*grpc.ClientConn, error) {
o := defaultOptions()
o.apply(opts...)
var clientOptions []grpc.DialOption
// service discovery
if o.discovery != nil {
clientOptions = append(clientOptions, grpc.WithResolvers(
discovery.NewBuilder(
o.discovery,
discovery.WithInsecure(o.discoveryInsecure),
)))
}
// load balance option
if o.enableLoadBalance {
clientOptions = append(clientOptions, grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`))
}
// secure option
so, err := secureOption(o)
if err != nil {
return nil, err
}
clientOptions = append(clientOptions, so)
// token option
if o.enableToken {
clientOptions = append(clientOptions, interceptor.ClientTokenOption(
o.appID,
o.appKey,
o.isSecure(),
))
}
// unary options
clientOptions = append(clientOptions, unaryClientOptions(o))
// stream options
clientOptions = append(clientOptions, streamClientOptions(o))
// custom options
clientOptions = append(clientOptions, o.dialOptions...)
return grpc.NewClient(endpoint, clientOptions...)
}
// Dial to grpc server
// Deprecated: use NewClient instead
func Dial(_ context.Context, endpoint string, opts ...Option) (*grpc.ClientConn, error) {
return NewClient(endpoint, opts...)
}
func secureOption(o *options) (grpc.DialOption, error) {
switch o.secureType {
case secureOneWay: // server side certification
if o.certFile == "" {
return nil, errors.New("cert file is empty")
}
credentials, err := gtls.GetClientTLSCredentials(o.serverName, o.certFile)
if err != nil {
return nil, err
}
return grpc.WithTransportCredentials(credentials), nil
case secureTwoWay: // both client and server side certification
if o.caFile == "" {
return nil, errors.New("ca file is empty")
}
if o.certFile == "" {
return nil, errors.New("cert file is empty")
}
if o.keyFile == "" {
return nil, errors.New("key file is empty")
}
credentials, err := gtls.GetClientTLSCredentialsByCA(
o.serverName,
o.caFile,
o.certFile,
o.keyFile,
)
if err != nil {
return nil, err
}
return grpc.WithTransportCredentials(credentials), nil
default:
return grpc.WithTransportCredentials(insecure.NewCredentials()), nil
}
}
func unaryClientOptions(o *options) grpc.DialOption {
var unaryClientInterceptors []grpc.UnaryClientInterceptor
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientRecovery())
if o.requestTimeout > 0 {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientTimeout(o.requestTimeout))
}
// request id
if o.enableRequestID {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientRequestID())
}
// logging
if o.enableLog {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientLog(logger.Get()))
}
// metrics
if o.enableMetrics {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientMetrics())
}
// circuit breaker
//if o.enableCircuitBreaker {
// unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientCircuitBreaker(
// // set rpc code for circuit breaker, default already includes codes.Internal and codes.Unavailable
// //interceptor.WithValidCode(codes.PermissionDenied),
// ))
//}
// retry
if o.enableRetry {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientRetry())
}
// trace
if o.enableTrace {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientTracing())
}
// custom unary interceptors
unaryClientInterceptors = append(unaryClientInterceptors, o.unaryInterceptors...)
return grpc.WithChainUnaryInterceptor(unaryClientInterceptors...)
}
func streamClientOptions(o *options) grpc.DialOption {
var streamClientInterceptors []grpc.StreamClientInterceptor
streamClientInterceptors = append(streamClientInterceptors, interceptor.StreamClientRecovery())
// request id
if o.enableRequestID {
streamClientInterceptors = append(streamClientInterceptors, interceptor.StreamClientRequestID())
}
// logging
if o.enableLog {
streamClientInterceptors = append(streamClientInterceptors, interceptor.StreamClientLog(logger.Get()))
}
// metrics
if o.enableMetrics {
streamClientInterceptors = append(streamClientInterceptors, interceptor.StreamClientMetrics())
}
// circuit breaker
//if o.enableCircuitBreaker {
// streamClientInterceptors = append(streamClientInterceptors, interceptor.StreamClientCircuitBreaker(
// // set rpc code for circuit breaker, default already includes codes.Internal and codes.Unavailable
// //interceptor.WithValidCode(codes.PermissionDenied),
// ))
//}
// retry
if o.enableRetry {
streamClientInterceptors = append(streamClientInterceptors, interceptor.StreamClientRetry())
}
// trace
if o.enableTrace {
streamClientInterceptors = append(streamClientInterceptors, interceptor.StreamClientTracing())
}
// custom stream interceptors
streamClientInterceptors = append(streamClientInterceptors, o.streamInterceptors...)
return grpc.WithChainStreamInterceptor(streamClientInterceptors...)
}
package grpccli
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
clientv3 "go.etcd.io/etcd/client/v3"
"go.uber.org/zap"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/gtls/certfile"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry/etcd"
)
func TestNewClient(t *testing.T) {
_, err := NewClient("localhost:8282")
assert.NoError(t, err)
_, err = Dial(context.Background(), "localhost:8282")
assert.NoError(t, err)
}
func TestNewClient2(t *testing.T) {
_, err := NewClient("localhost:8282",
WithEnableLog(zap.NewNop()),
WithEnableMetrics(),
WithToken(true, "grpc", "123456"),
WithEnableLoadBalance(),
WithEnableCircuitBreaker(),
WithEnableRetry(),
WithDiscovery(etcd.New(&clientv3.Client{})),
)
assert.NoError(t, err)
time.Sleep(time.Millisecond * 50)
}
func Test_unaryClientOptions(t *testing.T) {
o := &options{
enableToken: true,
enableLog: true,
enableRequestID: true,
enableTrace: true,
enableMetrics: true,
enableRetry: true,
enableLoadBalance: true,
enableCircuitBreaker: true,
}
scOpt := unaryClientOptions(o)
assert.NotNil(t, scOpt)
}
func Test_streamClientOptions(t *testing.T) {
o := &options{
enableToken: true,
enableLog: true,
enableRequestID: true,
enableTrace: true,
enableMetrics: true,
enableRetry: true,
enableLoadBalance: true,
enableCircuitBreaker: true,
}
scOpt := streamClientOptions(o)
assert.NotNil(t, scOpt)
}
func Test_secureOption(t *testing.T) {
o := &options{
secureType: "one-way",
serverName: "localhost",
certFile: certfile.Path("one-way/server.crt"),
}
// correct
opt, err := secureOption(o)
assert.NoError(t, err)
assert.NotNil(t, opt)
// error
o.certFile = ""
_, err = secureOption(o)
assert.Error(t, err)
o.certFile = "not found"
_, err = secureOption(o)
assert.Error(t, err)
o = &options{
secureType: "two-way",
serverName: "localhost",
caFile: certfile.Path("two-way/ca.pem"),
certFile: certfile.Path("two-way/client/client.pem"),
keyFile: certfile.Path("two-way/client/client.key"),
}
// correct
opt, err = secureOption(o)
assert.NoError(t, err)
assert.NotNil(t, opt)
// error
o.certFile = "not found"
_, err = secureOption(o)
assert.Error(t, err)
o.caFile = ""
_, err = secureOption(o)
assert.Error(t, err)
o.caFile = "not found"
o.certFile = ""
_, err = secureOption(o)
assert.Error(t, err)
o.certFile = "not found"
o.keyFile = ""
_, err = secureOption(o)
assert.Error(t, err)
o.secureType = ""
opt, err = secureOption(o)
assert.NoError(t, err)
assert.NotNil(t, opt)
}
package grpccli
import (
"time"
"go.uber.org/zap"
"google.golang.org/grpc"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
var (
secureOneWay = "one-way"
secureTwoWay = "two-way"
)
// Option grpc dial options
type Option func(*options)
// options grpc dial options
type options struct {
requestTimeout time.Duration // request timeout, valid only for unary
// secure setting
secureType string // secure type "","one-way","two-way"
serverName string // server name
caFile string // ca file
certFile string // cert file
keyFile string // key file
// token setting
enableToken bool // whether to turn on token
appID string
appKey string
// interceptor setting
enableLog bool // whether to turn on the log
log *zap.Logger
enableRequestID bool // whether to turn on the request id
enableTrace bool // whether to turn on tracing
enableMetrics bool // whether to turn on metrics
enableRetry bool // whether to turn on retry
enableLoadBalance bool // whether to turn on load balance
enableCircuitBreaker bool // whether to turn on circuit breaker
discovery registry.Discovery // if not nil means use service discovery
discoveryInsecure bool
// custom setting
dialOptions []grpc.DialOption // custom options
unaryInterceptors []grpc.UnaryClientInterceptor // custom unary interceptor
streamInterceptors []grpc.StreamClientInterceptor // custom stream interceptor
}
func defaultOptions() *options {
return &options{
secureType: "",
serverName: "localhost",
certFile: "",
keyFile: "",
caFile: "",
enableLog: false,
discoveryInsecure: true,
dialOptions: nil,
unaryInterceptors: nil,
streamInterceptors: nil,
discovery: nil,
}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithTimeout set dial timeout
func WithTimeout(d time.Duration) Option {
return func(o *options) {
o.requestTimeout = d
}
}
// WithEnableRequestID enable request id
func WithEnableRequestID() Option {
return func(o *options) {
o.enableRequestID = true
}
}
// WithEnableLog enable log
func WithEnableLog(log *zap.Logger) Option {
return func(o *options) {
o.enableLog = true
if log != nil {
o.log = log
return
}
o.log, _ = zap.NewProduction()
}
}
// WithEnableTrace enable trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithEnableMetrics enable metrics
func WithEnableMetrics() Option {
return func(o *options) {
o.enableMetrics = true
}
}
// WithEnableLoadBalance enable load balance
func WithEnableLoadBalance() Option {
return func(o *options) {
o.enableLoadBalance = true
}
}
// WithEnableRetry enable registry
func WithEnableRetry() Option {
return func(o *options) {
o.enableRetry = true
}
}
// WithEnableCircuitBreaker enable circuit breaker
func WithEnableCircuitBreaker() Option {
return func(o *options) {
o.enableCircuitBreaker = true
}
}
// WithDiscoveryInsecure setting discovery insecure
func WithDiscoveryInsecure(b bool) Option {
return func(o *options) {
o.discoveryInsecure = b
}
}
func (o *options) isSecure() bool {
if o.secureType == secureOneWay || o.secureType == secureTwoWay {
return true
}
return false
}
// WithSecure support setting one-way or two-way secure
func WithSecure(t string, serverName string, caFile string, certFile string, keyFile string) Option {
switch t {
case secureOneWay:
return WithOneWaySecure(serverName, certFile)
case secureTwoWay:
return WithTwoWaySecure(serverName, caFile, certFile, keyFile)
}
return func(o *options) {
o.secureType = t
}
}
// WithOneWaySecure set one-way secure
func WithOneWaySecure(serverName string, certFile string) Option {
return func(o *options) {
if serverName == "" {
serverName = "localhost"
}
o.secureType = secureOneWay
o.serverName = serverName
o.certFile = certFile
}
}
// WithTwoWaySecure set two-way secure
func WithTwoWaySecure(serverName string, caFile string, certFile string, keyFile string) Option {
return func(o *options) {
if serverName == "" {
serverName = "localhost"
}
o.secureType = secureTwoWay
o.serverName = serverName
o.caFile = caFile
o.certFile = certFile
o.keyFile = keyFile
}
}
// WithToken set token
func WithToken(enable bool, appID string, appKey string) Option {
return func(o *options) {
o.enableToken = enable
o.appID = appID
o.appKey = appKey
}
}
// WithDialOptions set dial options
func WithDialOptions(dialOptions ...grpc.DialOption) Option {
return func(o *options) {
o.dialOptions = append(o.dialOptions, dialOptions...)
}
}
// WithUnaryInterceptors set dial unaryInterceptors
func WithUnaryInterceptors(unaryInterceptors ...grpc.UnaryClientInterceptor) Option {
return func(o *options) {
o.unaryInterceptors = append(o.unaryInterceptors, unaryInterceptors...)
}
}
// WithStreamInterceptors set dial streamInterceptors
func WithStreamInterceptors(streamInterceptors ...grpc.StreamClientInterceptor) Option {
return func(o *options) {
o.streamInterceptors = append(o.streamInterceptors, streamInterceptors...)
}
}
// WithDiscovery set dial discovery
func WithDiscovery(discovery registry.Discovery) Option {
return func(o *options) {
o.discovery = discovery
}
}
package grpccli
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/interceptor"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
func TestWithDialOptions(t *testing.T) {
testData := grpc.WithTransportCredentials(insecure.NewCredentials())
opt := WithDialOptions(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.dialOptions[0])
}
func TestWithDiscovery(t *testing.T) {
testData := new(registry.Discovery)
opt := WithDiscovery(*testData)
o := new(options)
o.apply(opt)
assert.NotEqual(t, testData, o.discovery)
}
func TestWithEnableCircuitBreaker(t *testing.T) {
opt := WithEnableCircuitBreaker()
o := new(options)
o.apply(opt)
assert.Equal(t, true, o.enableCircuitBreaker)
}
func TestWithEnableLoadBalance(t *testing.T) {
opt := WithEnableLoadBalance()
o := new(options)
o.apply(opt)
assert.Equal(t, true, o.enableLoadBalance)
}
func TestWithEnableRequestID(t *testing.T) {
opt := WithEnableRequestID()
o := new(options)
o.apply(opt)
assert.Equal(t, true, o.enableRequestID)
}
func TestWithEnableLog(t *testing.T) {
opt := WithEnableLog(nil)
testData := zap.NewNop()
opt = WithEnableLog(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.log)
}
func TestWithEnableMetrics(t *testing.T) {
opt := WithEnableMetrics()
o := new(options)
o.apply(opt)
assert.Equal(t, true, o.enableMetrics)
}
func TestWithEnableRetry(t *testing.T) {
opt := WithEnableRetry()
o := new(options)
o.apply(opt)
assert.Equal(t, true, o.enableRetry)
}
func TestWithEnableTrace(t *testing.T) {
opt := WithEnableTrace()
o := new(options)
o.apply(opt)
assert.Equal(t, true, o.enableTrace)
}
func TestWithStreamInterceptors(t *testing.T) {
testData := interceptor.StreamClientRetry()
opt := WithStreamInterceptors(testData)
o := new(options)
o.apply(opt)
assert.LessOrEqual(t, 1, len(o.streamInterceptors))
}
func TestWithTimeout(t *testing.T) {
testData := time.Second
opt := WithTimeout(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.requestTimeout)
}
func TestWithDiscoveryInsecure(t *testing.T) {
var testData bool
opt := WithDiscoveryInsecure(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.discoveryInsecure)
}
func TestWithUnaryInterceptors(t *testing.T) {
testData := interceptor.UnaryClientRetry()
opt := WithUnaryInterceptors(testData)
o := new(options)
o.apply(opt)
assert.LessOrEqual(t, 1, len(o.unaryInterceptors))
}
func Test_defaultOptions(t *testing.T) {
o := defaultOptions()
assert.NotNil(t, o)
}
func Test_options_apply(t *testing.T) {
opt := WithEnableRetry()
o := new(options)
o.apply(opt)
assert.Equal(t, true, o.enableRetry)
}
func Test_options_isSecure(t *testing.T) {
o := new(options)
secure := o.isSecure()
assert.Equal(t, false, secure)
o.secureType = secureOneWay
secure = o.isSecure()
assert.Equal(t, true, secure)
}
func TestWithSecure(t *testing.T) {
o := new(options)
opt := WithSecure("foo", "", "", "", "")
o.apply(opt)
assert.Equal(t, "foo", o.secureType)
opt = WithSecure(secureOneWay, "", "", "", "")
o.apply(opt)
assert.Equal(t, secureOneWay, o.secureType)
opt = WithSecure(secureTwoWay, "", "", "", "")
o.apply(opt)
assert.Equal(t, secureTwoWay, o.secureType)
}
#!/bin/bash
# As SAN certificates are required for GO version 1.15 and above, steps to generate a SAN certificate
# copy /etc/pki/tls/openssl.cnf to the current directory and modify openssl.cnf
# (1) uncomment copy_extensions = copy
# (2) uncomment req_extensions = v3_req
# (3) add v3_req
# [ v3_req ]
# subjectAltName = @alt_names
# (4) add alt_names
# [alt_names]
# DNS.1 = localhost
# openssl.cnf file
opensslCnfFile=./openssl.cnf
# generating one-way authentication certificates
mkdir one-way && cd one-way
openssl genrsa -out server.key 2048
openssl req -new -x509 -days 3650 -sha256 -key server.key -out server.crt -subj "/C=cn/OU=custer/O=custer/CN=localhost" -config ${opensslCnfFile} -extensions v3_req
echo "A one-way certificate has been generated and is stored in the directory 'one-way'"
cd ..
# generate two-way authentication certificates
mkdir two-way && cd two-way
# generate a ca certificate
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 -key ca.key -out ca.pem -subj "/C=cn/OU=custer/O=custer/CN=localhost"
# server
openssl genpkey -algorithm RSA -out server.key
openssl req -new -nodes -key server.key -out server.csr -days 3650 -subj "/C=cn/OU=custer/O=custer/CN=localhost" -config ${opensslCnfFile} -extensions v3_req
openssl x509 -req -days 3650 -in server.csr -out server.pem -CA ca.pem -CAkey ca.key -CAcreateserial -extfile ${opensslCnfFile} -extensions v3_req
# client
openssl genpkey -algorithm RSA -out client.key
openssl req -new -nodes -key client.key -out client.csr -days 3650 -subj "/C=cn/OU=custer/O=custer/CN=localhost" -config ${opensslCnfFile} -extensions v3_req
openssl x509 -req -days 3650 -in client.csr -out client.pem -CA ca.pem -CAkey ca.key -CAcreateserial -extfile ${opensslCnfFile} -extensions v3_req
echo "A two-way certificate has been generated and is stored in the directory 'two-way'"
cd ..
// Package certfile is used to locate the certificate file.
package certfile
import (
"path/filepath"
"runtime"
)
var basepath string
func init() {
_, currentFile, _, _ := runtime.Caller(0) //nolint
basepath = filepath.Dir(currentFile)
}
// Path return absolute path
func Path(rel string) string {
if filepath.IsAbs(rel) {
return rel
}
return filepath.Join(basepath, rel)
}
package certfile
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPath(t *testing.T) {
testData := "README.md"
file := Path(testData)
assert.Equal(t, true, strings.Contains(file, testData))
}
-----BEGIN CERTIFICATE-----
MIIDOTCCAiGgAwIBAgIJAJ4oghzJ3CFQMA0GCSqGSIb3DQEBCwUAMEMxCzAJBgNV
BAYTAmNuMQ8wDQYDVQQLDAZjdXN0ZXIxDzANBgNVBAoMBmN1c3RlcjESMBAGA1UE
AwwJbG9jYWxob3N0MB4XDTIyMDUwOTEyMzUzMVoXDTMyMDUwNjEyMzUzMVowQzEL
MAkGA1UEBhMCY24xDzANBgNVBAsMBmN1c3RlcjEPMA0GA1UECgwGY3VzdGVyMRIw
EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQDeMaFH/Bp0bZdiWOKUYCr52TbZPXkXo//4I0urnH/PXdNrKhkiys4cP0byTY+Q
G7eVKY1qK70xnLxHzzjIcnZzWYUwrfBqopWmWKv3CFwjtv4/UXmnAI0uhx0pxXJD
ciX8hDo2opF0F/1WktVELYLnGqCWHgffYtshQDkVVR9zmBAjvSmS5G0J7sTtMIfs
P0Qvtoj9uJZLk+FjsUd3Rgy7/fooBYZp/m8rT2+HR0aIFRRDpTRKWhBO7RNC1s14
lBNp3pa9EnT0wu9egaRFzEhZsRiTbn8pMCYJVI71XG6ojp2cR9aWvyoTf72BOezF
fmQQuofBHbCkeFynreAgtyFNAgMBAAGjMDAuMBQGA1UdEQQNMAuCCWxvY2FsaG9z
dDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DANBgkqhkiG9w0BAQsFAAOCAQEAz47Q
+GL+dDhok98/vRCX4nUyCAJDwkWYroHk5pi9jMyB2XvGJiI8TMa7uS1zbCYqc9e7
oqhuquqscNnvLbESyLYPcOKfH9Lv671aMOKcSdLk9mDWOvWJXgjLwTMStOIr/maJ
2DfbRyXbeIuO3XfOJoBaUo0p/bT7pmshUpHtPLMvdTK0VN2pZ0UJzrEVcjFKLOrq
NzT+7df2pDXfOraSmgOX4qUOkRltNlEl1s2rxTyGchvQ4XX2V0BWTh1+eBmaA4y3
uneqvbiTgdBYUD55jI0lU4jPRqmJa/pn4A3yug9aZ5t04wU2Fts3N1iZZtAPy54V
wXRL+aYg5YlXNOvkYg==
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA3jGhR/wadG2XYljilGAq+dk22T15F6P/+CNLq5x/z13TayoZ
IsrOHD9G8k2PkBu3lSmNaiu9MZy8R884yHJ2c1mFMK3waqKVplir9whcI7b+P1F5
pwCNLocdKcVyQ3Il/IQ6NqKRdBf9VpLVRC2C5xqglh4H32LbIUA5FVUfc5gQI70p
kuRtCe7E7TCH7D9EL7aI/biWS5PhY7FHd0YMu/36KAWGaf5vK09vh0dGiBUUQ6U0
SloQTu0TQtbNeJQTad6WvRJ09MLvXoGkRcxIWbEYk25/KTAmCVSO9VxuqI6dnEfW
lr8qE3+9gTnsxX5kELqHwR2wpHhcp63gILchTQIDAQABAoIBAARcKPQGqGY8eEn/
wIJ02KMKdh8RK70hBLbTynpVmdRx3OIvc0rRe/Xl7/h1OSn0wUd2B0ZcEVxV3QPz
twOH63cb/JcV8q/E/PbEqqswSM6Smq6XZLG4Owz8rb/SFgnoxYIM/i9wRTZn+hqm
yvSJiBYM2bXYZQMnJ3Ghlv0qLHHSMrG24sF6MaaXGhWNIWURP5cYFaKclDWxYnb4
NqvbHUsHq/dosqg707VFcPM+AJuKOUbUS2yn9SDz0dEZZ31RYaby0IbWJY5H7WIK
ff7zOTgJK5/L+14VGqz19xuqzgRYp98R4AKa8gZWgDSSDTmtMWcu3vB08M0RxeK2
4xYEQcECgYEA9XrEF/LW36x2a+xUyq/ocUShm/OjrUheeEb4dRffOQJD4BdTI94V
7lCo7ZTkIijH/a1ZHRHKbq5qRXfeDepY0NR61Dgp0a93y+9voumAV1LE4mj4QvfV
hA4Kem7gbkm1GLFIegfJ0UyJ0ezWg5nBsVekb1AI88teutbbMLCdY8UCgYEA57dj
rNM5H1L4A6w2U0rxKzxdo6Tpv41xGWo5F6JUrquZp5Xwewuc3phU2JqbtPxmAhKZ
rSDLytoNM4g9ghlnqHCrIVgvB4PnRhPsLN4+Xw80vVa7rT/9t3fHFJk2yf7D+gYe
/0ytjqr8lJK+n8TvccCYqLMx8v4DJYA/BHVoN+kCgYA/YWAp8sgp3iSBPvaxknOI
czjqxCA5iFrj4ScbTHuVA5G01TDhLOEqs+a52NyCOVdRlyVQDRzgMOY9Y3KQ0zX1
TTcdfhbGDfD3Va3UOUeqxDMTZhjbaZPWUa3A6MnHj/5TDsrwvvwLryBSdDz6o4NQ
H5nUJ6z4DUU7JmhXjPJGJQKBgCaFX0Wo67MgaOi7ZWCdcdBdPbfpv44/plCyTwF1
1BwhnO62R389I/wBWQGVWlNRLmgjzXZxoE99xnoNqSQKsyfWayyk61SVggotX7Lp
03acIYlkdNjNbZFlkSwEeI5GEzBqeha5GZVBKbJNXvFSnPfNK9PHzoL15XIDivZj
ykqZAoGAe5D1yHrSicR0vN8xRbVkdn0gAFJ7Dj/hBnYeFWWdpJ5yPj73z93/cZoR
tERIEjvza6ne7VOvVCw8qB0xVSBPqphaTM1dmcGMOCx6+wLlkABWiAWGx7m9obcd
HEfAbRLVcHOjik+tYyrQE54dck9C1N+0tlMMYZSAhD/cFb8/OJA=
-----END RSA PRIVATE KEY-----
#
# OpenSSL example configuration file.
# This is mostly being used for generation of certificate requests.
#
# This definition stops the following lines choking if HOME isn't
# defined.
HOME = .
RANDFILE = $ENV::HOME/.rnd
# Extra OBJECT IDENTIFIER info:
#oid_file = $ENV::HOME/.oid
oid_section = new_oids
# To use this configuration file with the "-extfile" option of the
# "openssl x509" utility, name here the section containing the
# X.509v3 extensions to use:
# extensions =
# (Alternatively, use a configuration file that has only
# X.509v3 extensions in its main [= default] section.)
[ new_oids ]
# We can add new OIDs in here for use by 'ca', 'req' and 'ts'.
# Add a simple OID like this:
# testoid1=1.2.3.4
# Or use config file substitution like this:
# testoid2=${testoid1}.5.6
# Policies used by the TSA examples.
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7
####################################################################
[ ca ]
default_ca = CA_default # The default ca section
####################################################################
[ CA_default ]
dir = /etc/pki/CA # Where everything is kept
certs = $dir/certs # Where the issued certs are kept
crl_dir = $dir/crl # Where the issued crl are kept
database = $dir/index.txt # database index file.
#unique_subject = no # Set to 'no' to allow creation of
# several ctificates with same subject.
new_certs_dir = $dir/newcerts # default place for new certs.
certificate = $dir/cacert.pem # The CA certificate
serial = $dir/serial # The current serial number
crlnumber = $dir/crlnumber # the current crl number
# must be commented out to leave a V1 CRL
crl = $dir/crl.pem # The current CRL
private_key = $dir/private/cakey.pem# The private key
RANDFILE = $dir/private/.rand # private random number file
x509_extensions = usr_cert # The extentions to add to the cert
# Comment out the following two lines for the "traditional"
# (and highly broken) format.
name_opt = ca_default # Subject Name options
cert_opt = ca_default # Certificate field options
# Extension copying option: use with caution.
copy_extensions = copy
# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs
# so this is commented out by default to leave a V1 CRL.
# crlnumber must also be commented out to leave a V1 CRL.
# crl_extensions = crl_ext
default_days = 365 # how long to certify for
default_crl_days= 30 # how long before next CRL
default_md = sha256 # use SHA-256 by default
preserve = no # keep passed DN ordering
# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy = policy_match
# For the CA policy
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
####################################################################
[ req ]
default_bits = 2048
default_md = sha256
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert
# Passwords for private keys if not present they will be prompted for
# input_password = secret
# output_password = secret
# This sets a mask for permitted string types. There are several options.
# default: PrintableString, T61String, BMPString.
# pkix : PrintableString, BMPString (PKIX recommendation before 2004)
# utf8only: only UTF8Strings (PKIX recommendation after 2004).
# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
# MASK:XXXX a literal mask value.
# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
string_mask = utf8only
req_extensions = v3_req # The extensions to add to a certificate request
[ v3_req ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = XX
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
#stateOrProvinceName_default = Default Province
localityName = Locality Name (eg, city)
localityName_default = Default City
0.organizationName = Organization Name (eg, company)
0.organizationName_default = Default Company Ltd
# we can do this but it is not needed normally :-)
#1.organizationName = Second Organization Name (eg, company)
#1.organizationName_default = World Wide Web Pty Ltd
organizationalUnitName = Organizational Unit Name (eg, section)
#organizationalUnitName_default =
commonName = Common Name (eg, your name or your server\'s hostname)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64
# SET-ex3 = SET extension number 3
[ req_attributes ]
challengePassword = A challenge password
challengePassword_min = 4
challengePassword_max = 20
unstructuredName = An optional company name
[ usr_cert ]
# These extensions are added when 'ca' signs a request.
# This goes against PKIX guidelines but some CAs do it and some software
# requires this to avoid interpreting an end user certificate as a CA.
basicConstraints=CA:FALSE
# Here are some examples of the usage of nsCertType. If it is omitted
# the certificate can be used for anything *except* object signing.
# This is OK for an SSL server.
# nsCertType = server
# For an object signing certificate this would be used.
# nsCertType = objsign
# For normal client use this is typical
# nsCertType = client, email
# and for everything including object signing:
# nsCertType = client, email, objsign
# This is typical in keyUsage for a client certificate.
# keyUsage = nonRepudiation, digitalSignature, keyEncipherment
# This will be displayed in Netscape's comment listbox.
nsComment = "OpenSSL Generated Certificate"
# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
# This stuff is for subjectAltName and issuerAltname.
# Import the email address.
# subjectAltName=email:copy
# An alternative to produce certificates that aren't
# deprecated according to PKIX.
# subjectAltName=email:move
# Copy subject details
# issuerAltName=issuer:copy
#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem
#nsBaseUrl
#nsRevocationUrl
#nsRenewalUrl
#nsCaPolicyUrl
#nsSslServerName
# This is required for TSA certificates.
# extendedKeyUsage = critical,timeStamping
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
[ v3_ca ]
# Extensions for a typical CA
# PKIX recommendation.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
# This is what PKIX recommends but some broken software chokes on critical
# extensions.
#basicConstraints = critical,CA:true
# So we do this instead.
basicConstraints = CA:true
# Key usage: this is typical for a CA certificate. However since it will
# prevent it being used as an test self-signed certificate it is best
# left out by default.
# keyUsage = cRLSign, keyCertSign
# Some might want this also
# nsCertType = sslCA, emailCA
# Include email address in subject alt name: another PKIX recommendation
# subjectAltName=email:copy
# Copy issuer details
# issuerAltName=issuer:copy
# DER hex encoding of an extension: beware experts only!
# obj=DER:02:03
# Where 'obj' is a standard or added object
# You can even override a supported extension:
# basicConstraints= critical, DER:30:03:01:01:FF
[ crl_ext ]
# CRL extensions.
# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL.
# issuerAltName=issuer:copy
authorityKeyIdentifier=keyid:always
[ proxy_cert_ext ]
# These extensions should be added when creating a proxy certificate
# This goes against PKIX guidelines but some CAs do it and some software
# requires this to avoid interpreting an end user certificate as a CA.
basicConstraints=CA:FALSE
# Here are some examples of the usage of nsCertType. If it is omitted
# the certificate can be used for anything *except* object signing.
# This is OK for an SSL server.
# nsCertType = server
# For an object signing certificate this would be used.
# nsCertType = objsign
# For normal client use this is typical
# nsCertType = client, email
# and for everything including object signing:
# nsCertType = client, email, objsign
# This is typical in keyUsage for a client certificate.
# keyUsage = nonRepudiation, digitalSignature, keyEncipherment
# This will be displayed in Netscape's comment listbox.
nsComment = "OpenSSL Generated Certificate"
# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
# This stuff is for subjectAltName and issuerAltname.
# Import the email address.
# subjectAltName=email:copy
# An alternative to produce certificates that aren't
# deprecated according to PKIX.
# subjectAltName=email:move
# Copy subject details
# issuerAltName=issuer:copy
#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem
#nsBaseUrl
#nsRevocationUrl
#nsRenewalUrl
#nsCaPolicyUrl
#nsSslServerName
# This really needs to be in place for it to be a proxy certificate.
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo
####################################################################
[ tsa ]
default_tsa = tsa_config1 # the default TSA section
[ tsa_config1 ]
# These are used by the TSA reply generation only.
dir = ./demoCA # TSA root directory
serial = $dir/tsaserial # The current serial number (mandatory)
crypto_device = builtin # OpenSSL engine to use for signing
signer_cert = $dir/tsacert.pem # The TSA signing certificate
# (optional)
certs = $dir/cacert.pem # Certificate chain to include in reply
# (optional)
signer_key = $dir/private/tsakey.pem # The TSA private key (optional)
default_policy = tsa_policy1 # Policy if request did not specify it
# (optional)
other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional)
digests = sha1, sha256, sha384, sha512 # Acceptable message digests (mandatory)
accuracy = secs:1, millisecs:500, microsecs:100 # (optional)
clock_precision_digits = 0 # number of digits after dot. (optional)
ordering = yes # Is ordering defined for timestamps?
# (optional, default: no)
tsa_name = yes # Must the TSA name be included in the reply?
# (optional, default: no)
ess_cert_id_chain = no # Must the ESS cert id chain be included?
# (optional, default: no)
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArauYhYSNx9u6ErhA12/tw2tmHvi5uTNTjWPEpIyNl37K4ULX
alrteeS1cyu9sOE7LNTyMiHAPuIdFtyIm7i2JTGVe5+VB6bhMfqbB2G9jhGcUHNB
xcF8vIOg2AZ/6Z7pQ6npAt2DhP1SfadQ3CI+too3EGWdQaodTXVGpVbOlx9NhKah
KtfgZOjh2Y80enCC7+OF69qSUpXZcM4iFTpZIIBIx3aeoGdHEOU7m//v4D2Jokvb
zWhQYl95D8jYkucikHSdOpfklbGmgovXNjTrS6/Rtyr/EOTPY9TMZlervnlRCa9p
2p4HOTK5mIUkzhZ+WShbGnWQmUELvNGtEuZPiQIDAQABAoIBAClKqULll0gzh/Q3
dsNCS4exG2C1xoKwH2d4lyKAgJHKhbY1TD5vTBM1O6cceUd65bTtFICy4aCR5lSv
LpPHwRbqyR3RfX+KP/TAHugMZyNGMKI4JSU9scioiy+RrAwBynhaB5q0zDZsfJZ/
l4wfYEXKavktD8yzA7CM97UMBKaNWHzoEKWWtRVAYemA9JRhr5OO8aKMe+ug2qeu
ppJe8JzrBc6mlPs4hRXjIVE9ybKgDoMImGH1eP80Wrj5RQ1ZdyDypoPmzBac0yyQ
lB80DkT8rCTAsswvvdiPkaCNsbm12uHMMOlasrvVGHBih+Cz5G6nI5u4IfSePJ0h
L143iXkCgYEA1WKdgBFjwEQANH+xqF+W7mVCNTTkaQAL646T/97ZBsVvgJDxKvEO
fTLlX4+KSei8IULIfcZal5cqFNPA0UvpEVHc9+aq9oIZQ2a+RRx4UJAf+5gTVFjZ
k4jnece4XWX2qg67j1BLD+3SsxFpx6EXLvSTpoezpT9KDRwe//UGpEMCgYEA0FqL
BqdyC1zZ+GbMIYXl9/NfzxBNOoFqP8IR8ZCu4mBlBOhpEXlvMWW76+FdL6txOQGV
6La//vffsq24vZk6hN3T8ykOeoXtdguKyoWkEV4IA5L4ZJ4LRCFrAMzVFzgWAzg3
RHu9yYv6Qiyht78GIEPT/ITlP1urmOAmoB1VRkMCgYB22URMDmN2tOlAVFcJJqSU
B0YHCHynluUMwA7ilqZeRR1DiHcqqbSeOvjSbsphPAV8qQuuMgpHIGTJ0N82M4eO
o//k+08BmZikl9cl+yNwC7YklaE+e3ZD3B7BD2I6cw4dzbLdsaT9LEMMhYhbLfgR
qRuLx01hnoyKHL2PZlParQKBgCQ7qrO0iOOq+Qj2r4cg5vYwr7etqRCEkvqVgFNX
CuK5SrgIxsTQLmMTwxNpNLBmiyETwoMezNLFPnSvO1JVhFS40vQVbrwl8D64ESHZ
DcgrZw9gDqxIw8LMYPRZqrEIAuP6cboKHt4e5p19b34pzSHtSb8/STK0eWnziSQN
FdhLAoGADvOI+DM3VBP+Y/tMu0WfYgJVvfvWDoFrTPtf4otmqmlxoQ2cdgSx+WMv
Hm7B+x+8GhNZcqNmEYBEVTdyjoxmgCcTaNDzsCN2NNlUUOdFVoOztGXUnrswaRc5
qa8GdWYUIEdZTsKWAE0HPr0i4P+ck/F67mUfrdUrzTfMSp0nUhw=
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDWTCCAkGgAwIBAgIJALDASGB2020CMA0GCSqGSIb3DQEBCwUAMEMxCzAJBgNV
BAYTAmNuMQ8wDQYDVQQLDAZjdXN0ZXIxDzANBgNVBAoMBmN1c3RlcjESMBAGA1UE
AwwJbG9jYWxob3N0MB4XDTIyMDUwOTEyMzUzMVoXDTMyMDUwNjEyMzUzMVowQzEL
MAkGA1UEBhMCY24xDzANBgNVBAsMBmN1c3RlcjEPMA0GA1UECgwGY3VzdGVyMRIw
EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCtq5iFhI3H27oSuEDXb+3Da2Ye+Lm5M1ONY8SkjI2XfsrhQtdqWu155LVzK72w
4Tss1PIyIcA+4h0W3IibuLYlMZV7n5UHpuEx+psHYb2OEZxQc0HFwXy8g6DYBn/p
nulDqekC3YOE/VJ9p1DcIj62ijcQZZ1Bqh1NdUalVs6XH02EpqEq1+Bk6OHZjzR6
cILv44Xr2pJSldlwziIVOlkggEjHdp6gZ0cQ5Tub/+/gPYmiS9vNaFBiX3kPyNiS
5yKQdJ06l+SVsaaCi9c2NOtLr9G3Kv8Q5M9j1MxmV6u+eVEJr2nangc5MrmYhSTO
Fn5ZKFsadZCZQQu80a0S5k+JAgMBAAGjUDBOMB0GA1UdDgQWBBS5eLpBhPh0JtMI
8xBBb0VX+Q98zDAfBgNVHSMEGDAWgBS5eLpBhPh0JtMI8xBBb0VX+Q98zDAMBgNV
HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAJN5lIRwOQIAsVR7TSipyCh7/L
oIdfWvH2wDi457UBvZdvWPI7o8pfMSO3ChBmoDPxo1sK0VejEnXnFLXIh7KHd9IG
BLeAxAXM0EZ8crTc1XItnVBvqWSDHHFBthPfFXNC0fDBfrkoHpiiS6LaVTYgDUV2
JK21cJ9zRF0r0VSGktPN+2bvVdozJj14jhK62ewdxEzOaJw19v9Y9T/834q0nhW9
5EFhSEQU4VBmoGyZGDiOnp+jk0/ffMu6AkQbu9xUgRGZ8acnYx/A7Bbv2hhSDyrZ
bPeFbJhxNzdRmryfffRYlepZCZ8fzgGynVFeXS4an/DZxVKPqSNwPzQPHoso
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE REQUEST-----
MIIBwjCCASsCAQAwQzELMAkGA1UEBhMCY24xDzANBgNVBAsMBmN1c3RlcjEPMA0G
A1UECgwGY3VzdGVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB
BQADgY0AMIGJAoGBAMVr7hmv1HQoipJa5ANdi2ipHtXZVzqPPWbZv30TKjAp1H7s
NxoDOpKuerNj9X6HwsPhbvykAfdlrNSw39Ied+0Z5gIIVZi3H/FEuT/cJeNEU51G
J3cu+BEtK5hs1gpv0AAsQKYNhzj/wU1rePUW7VPOYuj7cxnCUyxqa4EvjMRzAgMB
AAGgPzA9BgkqhkiG9w0BCQ4xMDAuMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAJBgNV
HRMEAjAAMAsGA1UdDwQEAwIF4DANBgkqhkiG9w0BAQsFAAOBgQBTr9Hv6p6eutgf
WBB8YsAEGwy2l1Zm9/EaR8TDxUwivSz+/O5v1bLk3KUSF13A765K8w4k+zs6ZjTE
Z7hTA66g+D+QUfNFLNtG2noZd6s9Z4pOQ5/0UswmE7N45rKjPJrasGaKn4pF6hbo
YNSrbIm/+C3WkSamv86YXSjI3CZZNw==
-----END CERTIFICATE REQUEST-----
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMVr7hmv1HQoipJa
5ANdi2ipHtXZVzqPPWbZv30TKjAp1H7sNxoDOpKuerNj9X6HwsPhbvykAfdlrNSw
39Ied+0Z5gIIVZi3H/FEuT/cJeNEU51GJ3cu+BEtK5hs1gpv0AAsQKYNhzj/wU1r
ePUW7VPOYuj7cxnCUyxqa4EvjMRzAgMBAAECgYACzrogG2QGEt2Fn01GzvnAj0ck
+2ZGKutQnyAeAzvCW5XuCCXwdMNMera7/lvrZLrcVkRhy2NLxWJj0/Aa6NwDuMbi
pUgNIXvtn+2du3FtDMuyAJeSXPmyDO9MAfwceshCSULqwmr8G2MPfycmFwliGf5W
E2RbdmHoq/tlaoEYQQJBAO5J5gmZwvN7eCTe5s24kuKr6QBTo/Vnfd0YZq65H0eZ
+VKO7ABkxaOoSTw2PCENAnNCTNmGAaarYJLOmd+v03sCQQDUGGwnvnfzsecI/Qz0
SgUPDARmW0DyId+ADFfYcwo5lmuDqgexdluipNwF4bHElDGqHUtzqUnlcR7TMwsM
JuVpAkBlF/z8PbuzyMIkAl0xEglfGUf014dL6ehAEMYfgnJ+0hgwqmn9kMM4t4C6
htfjvb04YPxxnKS+rR5/qh8mA1ZZAkA1fTNJkq+NtgAeNDNgKIq+ELnLVpg6eHB1
Sqec3uZlP5o9ylPGXaMekZUrpo++k+EyldDBiqAoTS8I9IaEugqBAkEAgUtHWQqh
tHZqAJf9Trp6vrCue3YySmv7Z2RnUAjbEiCF2arQBurawmH4MRDozk+28UL9DUsl
NRlhp8+OEuhoQQ==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICtTCCAZ2gAwIBAgIJAJRGbxsxpd2HMA0GCSqGSIb3DQEBCwUAMEMxCzAJBgNV
BAYTAmNuMQ8wDQYDVQQLDAZjdXN0ZXIxDzANBgNVBAoMBmN1c3RlcjESMBAGA1UE
AwwJbG9jYWxob3N0MB4XDTIyMDUwOTEyMzUzMVoXDTMyMDUwNjEyMzUzMVowQzEL
MAkGA1UEBhMCY24xDzANBgNVBAsMBmN1c3RlcjEPMA0GA1UECgwGY3VzdGVyMRIw
EAYDVQQDDAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMVr
7hmv1HQoipJa5ANdi2ipHtXZVzqPPWbZv30TKjAp1H7sNxoDOpKuerNj9X6HwsPh
bvykAfdlrNSw39Ied+0Z5gIIVZi3H/FEuT/cJeNEU51GJ3cu+BEtK5hs1gpv0AAs
QKYNhzj/wU1rePUW7VPOYuj7cxnCUyxqa4EvjMRzAgMBAAGjMDAuMBQGA1UdEQQN
MAuCCWxvY2FsaG9zdDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DANBgkqhkiG9w0B
AQsFAAOCAQEArVAzVK2GAlWRYNdPOBp097136xrWw932Yn2MbB+Bv+QQmGdhQrxJ
jhbaPUw+as9LkLAeohxx9hpNO/VkMxWC1sWEyU8xYcCR1ym3Z4+qNMiMnBEUPHCs
8Ox8IqRKcmOrHfM7UT/TjjIbkh2Sc/mXZThjSEfFWBNqz1DnW8/FrJGVkY4kZT6u
wPgDanCjPc7udFy86HCP4dVl3YFeoIV2qSzQWnVqDSGbtYgyL+iFJVPlctmHl5q+
37cp8hxN6PaANUQNX0GvGrKNr5eURxdc5c6djx5O8+j8ouwjsWOoZxptiFkA34et
mmbOsHt7/fqL2T3EiUuNBIS/um6FnM0lIw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE REQUEST-----
MIIBwjCCASsCAQAwQzELMAkGA1UEBhMCY24xDzANBgNVBAsMBmN1c3RlcjEPMA0G
A1UECgwGY3VzdGVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB
BQADgY0AMIGJAoGBAKyyEDoTJ2kVVupWFdrrILH4DdLprYt/jzpp6qgGuyD5xysV
mrqmspKkh6GUEKwnBdAi7qR3OCPmaUBQUI4R++rKbnZLkynb4Aq2S7rEtRglhz8g
ThM3H90V7nwdj0EzkIXaSfcX6PAOEDK1tDw75krIzXFj7qW/xyPYwcff42L3AgMB
AAGgPzA9BgkqhkiG9w0BCQ4xMDAuMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAJBgNV
HRMEAjAAMAsGA1UdDwQEAwIF4DANBgkqhkiG9w0BAQsFAAOBgQBrE6nQ0WVad0Zh
bpC3J/DtW6kECeTMK+oWA7TpRAKs6fhCfimhpJlp5JyaLeOXx1CXEI7OEpH1VPMr
2bHdSFs5PpadRUJlLNUtcA+gheCOqJWmx6xtFu/Jc+8jYaNj6nkMRQrjAsQomvRB
Xtz308RZhEPk8U7ZcNZKM0NMJM+v2A==
-----END CERTIFICATE REQUEST-----
-----BEGIN PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAKyyEDoTJ2kVVupW
FdrrILH4DdLprYt/jzpp6qgGuyD5xysVmrqmspKkh6GUEKwnBdAi7qR3OCPmaUBQ
UI4R++rKbnZLkynb4Aq2S7rEtRglhz8gThM3H90V7nwdj0EzkIXaSfcX6PAOEDK1
tDw75krIzXFj7qW/xyPYwcff42L3AgMBAAECgYABJBgJWjELd7GgULtKO/12T44/
031rC4e1uhdrzseTuzK+rSDdlNZfM6kVvzWw/X0DWAe9nHAJhK8zVpSBq8q/Sphi
ie2hvGDBzbXiDcm/bgRDwSdaHzGEd+G13KKOjbL6azgZ60rd4WcxpNshNPYsDzUM
LFfE9Fs0N3tFMe3U8QJBAOAUMGUtYU5pgR5NH+TGCGz0hmEsYRGhuL0AS7T2iAKE
3KN9uqH9YMsDO7kvAxR0VDzLpeb+pAvF0ej26KY3xAkCQQDFTAEiAqgHjhRBRRmX
VJ+HF0qIFbVfDgJpYQnGWoiPU7h4Hb5MEeMOY0FzfYAMBlkOmrJtAiimUDSPueru
ka7/AkBJz9yxN2WaQr65kIY1Ada8rT+musuu1yrXd0V48sySp9lWMZBM0/4SYZpG
wemtzzQAYsTfdnnrNtqSduLj/fKhAkBE6yjWJZFmmjXvGuE2oKPdP8CUSukBXFZx
Uylj9YoQbxFYPCOWVQU4qGlbm3JYQPtpA5biR0fF2OyTbEFpttAnAkASJ7+Gle61
b7F4mzbMpuEjsMmf7negz6WCmTLGElOEFFUgJvODTDI02wd/LYSfoPbkdK3OFq8i
+KNNEswq3B8m
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICtTCCAZ2gAwIBAgIJAJRGbxsxpd2GMA0GCSqGSIb3DQEBCwUAMEMxCzAJBgNV
BAYTAmNuMQ8wDQYDVQQLDAZjdXN0ZXIxDzANBgNVBAoMBmN1c3RlcjESMBAGA1UE
AwwJbG9jYWxob3N0MB4XDTIyMDUwOTEyMzUzMVoXDTMyMDUwNjEyMzUzMVowQzEL
MAkGA1UEBhMCY24xDzANBgNVBAsMBmN1c3RlcjEPMA0GA1UECgwGY3VzdGVyMRIw
EAYDVQQDDAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKyy
EDoTJ2kVVupWFdrrILH4DdLprYt/jzpp6qgGuyD5xysVmrqmspKkh6GUEKwnBdAi
7qR3OCPmaUBQUI4R++rKbnZLkynb4Aq2S7rEtRglhz8gThM3H90V7nwdj0EzkIXa
SfcX6PAOEDK1tDw75krIzXFj7qW/xyPYwcff42L3AgMBAAGjMDAuMBQGA1UdEQQN
MAuCCWxvY2FsaG9zdDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DANBgkqhkiG9w0B
AQsFAAOCAQEAnd2KBxIO96Xf8DGcuaCcjD4MKs5+ZREZ8HGNSq/aORR3Mk1pEc/W
zJntbgQbjV42kjW8aBpKDVZ0uZA1tTAlqbOJNEKn/f4s9JJ86gCb52Uwp+bE/a74
7snaALEAXaS/j8Ag7cge+ssq/o4cD9you/TMBjvs4xNRPMaQ1CxhJ1hr1BndSR7u
oUVWkTf9QfLMEB6OcLmgoH1PfDw7XUEws5GyHnaj72rI1qa6gLyyGJqiA4/Xp34G
AXn3jLELPeQoixIvuTBMgc6cF94BU7UbuSFtywj/B/u7hqhSLDbXtzSjCFJcAMVT
omPY5F+fFVt/Sfe1CZHvQ5eepo0Yjd2UPQ==
-----END CERTIFICATE-----
package gtls
import (
"crypto/tls"
"crypto/x509"
"os"
"google.golang.org/grpc/credentials"
)
// GetClientTLSCredentialsByCA two-way authentication via CA-issued root certificate
func GetClientTLSCredentialsByCA(serverName string, caFile string, certFile string, keyFile string) (credentials.TransportCredentials, error) {
// read and parse the information from the certificate file to obtain the certificate public key, key pair
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
// create an empty CertPool
certPool := x509.NewCertPool()
ca, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
// attempts to parse the incoming PEM-encoded certificate. If the parsing is successful it will be added to the CertPool for later use
if ok := certPool.AppendCertsFromPEM(ca); !ok {
return nil, err
}
// building TLS-based TransportCredentials options
c := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert}, // set up a certificate chain that allows the inclusion of one or more
ServerName: serverName, // requirement to verify the client's certificate
RootCAs: certPool,
})
return c, err
}
// GetClientTLSCredentials TLS encryption
func GetClientTLSCredentials(serverName string, certFile string) (credentials.TransportCredentials, error) {
c, err := credentials.NewClientTLSFromFile(certFile, serverName)
if err != nil {
return nil, err
}
return c, err
}
package gtls
import (
"testing"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/gtls/certfile"
"github.com/stretchr/testify/assert"
)
func TestGetClientTLSCredentials(t *testing.T) {
credentials, err := GetClientTLSCredentials("localhost", certfile.Path("one-way/server.crt"))
assert.NoError(t, err)
assert.NotNil(t, credentials)
_, err = GetClientTLSCredentials("localhost", certfile.Path("one-way/notfound.crt"))
assert.Error(t, err)
}
func TestGetClientTLSCredentialsByCA(t *testing.T) {
credentials, err := GetClientTLSCredentialsByCA(
"localhost",
certfile.Path("two-way/ca.pem"),
certfile.Path("two-way/client/client.pem"),
certfile.Path("two-way/client/client.key"),
)
assert.NoError(t, err)
assert.NotNil(t, credentials)
_, err = GetClientTLSCredentialsByCA(
"localhost",
certfile.Path("two-way/ca.pem"),
certfile.Path("two-way/client/notfound.pem"),
certfile.Path("two-way/client/notfound.key"),
)
assert.Error(t, err)
_, err = GetClientTLSCredentialsByCA(
"localhost",
certfile.Path("two-way/notfound.pem"),
certfile.Path("two-way/client/client.pem"),
certfile.Path("two-way/client/client.key"),
)
assert.Error(t, err)
}
// Package gtls provides grpc secure connectivity, supporting both server-only authentication and client-server authentication.
package gtls
import (
"crypto/tls"
"crypto/x509"
"errors"
"os"
"google.golang.org/grpc/credentials"
)
// GetServerTLSCredentialsByCA two-way authentication via CA-issued root certificate
func GetServerTLSCredentialsByCA(caFile string, certFile string, keyFile string) (credentials.TransportCredentials, error) {
//read and parse the information from the certificate file to obtain the certificate public key, key pair
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
// create an empty CertPool
certPool := x509.NewCertPool()
ca, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
//attempts to parse the incoming PEM-encoded certificate. If the parsing is successful it will be added to the CertPool for later use
if ok := certPool.AppendCertsFromPEM(ca); !ok {
return nil, errors.New("certPool.AppendCertsFromPEM err")
}
//building TLS-based TransportCredentials options
c := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert}, // set up a certificate chain that allows the inclusion of one or more
ClientAuth: tls.RequireAndVerifyClientCert, // requirement to verify the client's certificate
ClientCAs: certPool, // set the set of root certificates and use the mode set in ClientAuth for verification
})
return c, err
}
// GetServerTLSCredentials server-side authentication
func GetServerTLSCredentials(certFile string, keyFile string) (credentials.TransportCredentials, error) {
c, err := credentials.NewServerTLSFromFile(certFile, keyFile)
if err != nil {
return nil, err
}
return c, err
}
package gtls
import (
"testing"
"github.com/stretchr/testify/assert"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/gtls/certfile"
)
func TestGetServerTLSCredentials(t *testing.T) {
credentials, err := GetServerTLSCredentials(certfile.Path("one-way/server.crt"), certfile.Path("one-way/server.key"))
assert.NoError(t, err)
assert.NotNil(t, credentials)
_, err = GetServerTLSCredentials(certfile.Path("one-way/notfound.crt"), certfile.Path("one-way/notfound.key"))
assert.Error(t, err)
}
func TestGetServerTLSCredentialsByCA(t *testing.T) {
credentials, err := GetServerTLSCredentialsByCA(
certfile.Path("two-way/ca.pem"),
certfile.Path("two-way/server/server.pem"),
certfile.Path("two-way/server/server.key"),
)
assert.NoError(t, err)
assert.NotNil(t, credentials)
_, err = GetServerTLSCredentialsByCA(
certfile.Path("two-way/ca.pem"),
certfile.Path("two-way/server/notfound.pem"),
certfile.Path("two-way/server/notfound.key"),
)
assert.Error(t, err)
_, err = GetServerTLSCredentialsByCA(
certfile.Path("two-way/notfound.pem"),
certfile.Path("two-way/server/server.pem"),
certfile.Path("two-way/server/server.key"),
)
assert.Error(t, err)
}
// Package interceptor provides commonly used grpc client-side and server-side interceptors.
package interceptor
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gitlab.wanzhuangkj.com/tush/xpkg/container/group"
"gitlab.wanzhuangkj.com/tush/xpkg/errcode"
"gitlab.wanzhuangkj.com/tush/xpkg/shield/circuitbreaker"
)
// ErrNotAllowed error not allowed.
var ErrNotAllowed = circuitbreaker.ErrNotAllowed
// CircuitBreakerOption set the circuit breaker circuitBreakerOptions.
type CircuitBreakerOption func(*circuitBreakerOptions)
type circuitBreakerOptions struct {
group *group.Group
// rpc code for circuit breaker, default already includes codes.Internal and codes.Unavailable
validCodes map[codes.Code]struct{}
// degrade handler for unary server
unaryServerDegradeHandler func(ctx context.Context, req interface{}) (reply interface{}, error error)
}
func defaultCircuitBreakerOptions() *circuitBreakerOptions {
return &circuitBreakerOptions{
group: group.NewGroup(func() interface{} {
return circuitbreaker.NewBreaker()
}),
validCodes: map[codes.Code]struct{}{
codes.Internal: {},
codes.Unavailable: {},
},
}
}
func (o *circuitBreakerOptions) apply(opts ...CircuitBreakerOption) {
for _, opt := range opts {
opt(o)
}
}
// WithGroup with circuit breaker group.
// NOTE: implements generics circuitbreaker.CircuitBreaker
func WithGroup(g *group.Group) CircuitBreakerOption {
return func(o *circuitBreakerOptions) {
if g != nil {
o.group = g
}
}
}
// WithValidCode rpc code to mark failed
func WithValidCode(code ...codes.Code) CircuitBreakerOption {
return func(o *circuitBreakerOptions) {
for _, c := range code {
o.validCodes[c] = struct{}{}
}
}
}
// WithUnaryServerDegradeHandler unary server degrade handler function
func WithUnaryServerDegradeHandler(handler func(ctx context.Context, req interface{}) (reply interface{}, error error)) CircuitBreakerOption {
return func(o *circuitBreakerOptions) {
o.unaryServerDegradeHandler = handler
}
}
// UnaryClientCircuitBreaker client-side unary circuit breaker interceptor
func UnaryClientCircuitBreaker(opts ...CircuitBreakerOption) grpc.UnaryClientInterceptor {
o := defaultCircuitBreakerOptions()
o.apply(opts...)
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
breaker := o.group.Get(method).(circuitbreaker.CircuitBreaker)
if err := breaker.Allow(); err != nil {
// NOTE: when client reject request locally, keep adding counter let the drop ratio higher.
breaker.MarkFailed()
return errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
}
err := invoker(ctx, method, req, reply, cc, opts...)
if err != nil {
// NOTE: need to check internal and service unavailable error
s, ok := status.FromError(err)
_, isHit := o.validCodes[s.Code()]
if ok && isHit {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
return err
}
}
// StreamClientCircuitBreaker client-side stream circuit breaker interceptor
func StreamClientCircuitBreaker(opts ...CircuitBreakerOption) grpc.StreamClientInterceptor {
o := defaultCircuitBreakerOptions()
o.apply(opts...)
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
breaker := o.group.Get(method).(circuitbreaker.CircuitBreaker)
if err := breaker.Allow(); err != nil {
// NOTE: when client reject request locally, keep adding counter let the drop ratio higher.
breaker.MarkFailed()
return nil, errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
}
clientStream, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
// NOTE: need to check internal and service unavailable error
s, ok := status.FromError(err)
_, isHit := o.validCodes[s.Code()]
if ok && isHit {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
return clientStream, err
}
}
// UnaryServerCircuitBreaker server-side unary circuit breaker interceptor
func UnaryServerCircuitBreaker(opts ...CircuitBreakerOption) grpc.UnaryServerInterceptor {
o := defaultCircuitBreakerOptions()
o.apply(opts...)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
breaker := o.group.Get(info.FullMethod).(circuitbreaker.CircuitBreaker)
if err := breaker.Allow(); err != nil {
// NOTE: when client reject request locally, keep adding let the drop ratio higher.
breaker.MarkFailed()
if o.unaryServerDegradeHandler != nil {
return o.unaryServerDegradeHandler(ctx, req)
}
return nil, errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
}
reply, err := handler(ctx, req)
if err != nil {
// NOTE: need to check internal and service unavailable error
s, ok := status.FromError(err)
_, isHit := o.validCodes[s.Code()]
if ok && isHit {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
return reply, err
}
}
// StreamServerCircuitBreaker server-side stream circuit breaker interceptor
func StreamServerCircuitBreaker(opts ...CircuitBreakerOption) grpc.StreamServerInterceptor {
o := defaultCircuitBreakerOptions()
o.apply(opts...)
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
breaker := o.group.Get(info.FullMethod).(circuitbreaker.CircuitBreaker)
if err := breaker.Allow(); err != nil {
// NOTE: when client reject request locally, keep adding counter let the drop ratio higher.
breaker.MarkFailed()
return errcode.StatusServiceUnavailable.ToRPCErr(err.Error())
}
err := handler(srv, ss)
if err != nil {
// NOTE: need to check internal and service unavailable error
s, ok := status.FromError(err)
_, isHit := o.validCodes[s.Code()]
if ok && isHit {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
return err
}
}
package interceptor
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"gitlab.wanzhuangkj.com/tush/xpkg/container/group"
"gitlab.wanzhuangkj.com/tush/xpkg/errcode"
"gitlab.wanzhuangkj.com/tush/xpkg/shield/circuitbreaker"
"google.golang.org/grpc/codes"
)
func TestUnaryClientCircuitBreaker(t *testing.T) {
interceptor := UnaryClientCircuitBreaker(
WithGroup(group.NewGroup(func() interface{} {
return circuitbreaker.NewBreaker()
})),
WithValidCode(codes.PermissionDenied),
)
assert.NotNil(t, interceptor)
ivoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
return errcode.StatusInternalServerError.ToRPCErr()
}
for i := 0; i < 110; i++ {
err := interceptor(context.Background(), "/test", nil, nil, nil, ivoker)
assert.Error(t, err)
}
ivoker = func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
return errcode.StatusInvalidParams.Err()
}
err := interceptor(context.Background(), "/test", nil, nil, nil, ivoker)
assert.Error(t, err)
}
func TestSteamClientCircuitBreaker(t *testing.T) {
interceptor := StreamClientCircuitBreaker()
assert.NotNil(t, interceptor)
streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, errcode.StatusInternalServerError.ToRPCErr()
}
for i := 0; i < 110; i++ {
_, err := interceptor(context.Background(), nil, nil, "/test", streamer)
assert.Error(t, err)
}
streamer = func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, errcode.StatusInvalidParams.Err()
}
_, err := interceptor(context.Background(), nil, nil, "/test", streamer)
assert.Error(t, err)
}
func TestUnaryServerCircuitBreaker(t *testing.T) {
degradeHandler := func(ctx context.Context, req interface{}) (reply interface{}, error error) {
return "degrade", errcode.StatusSuccess.ToRPCErr()
}
interceptor := UnaryServerCircuitBreaker(WithUnaryServerDegradeHandler(degradeHandler))
assert.NotNil(t, interceptor)
count := 0
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
count++
if count%2 == 0 {
return nil, errcode.StatusSuccess.ToRPCErr()
}
return nil, errcode.StatusInternalServerError.ToRPCErr()
}
successCount, failCount, degradeCount := 0, 0, 0
for i := 0; i < 1000; i++ {
reply, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/test"}, handler)
if err != nil {
failCount++
continue
}
if reply == "degrade" {
degradeCount++
} else {
successCount++
}
}
t.Logf("successCount: %d, failCount: %d, degradeCount: %d", successCount, failCount, degradeCount)
handler = func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, errcode.StatusInvalidParams.Err()
}
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/test"}, handler)
t.Log(err)
}
func TestSteamServerCircuitBreaker(t *testing.T) {
interceptor := StreamServerCircuitBreaker()
assert.NotNil(t, interceptor)
handler := func(srv interface{}, stream grpc.ServerStream) error {
return errcode.StatusInternalServerError.ToRPCErr()
}
for i := 0; i < 110; i++ {
err := interceptor(nil, nil, &grpc.StreamServerInfo{FullMethod: "/test"}, handler)
assert.Error(t, err)
}
handler = func(srv interface{}, stream grpc.ServerStream) error {
return errcode.StatusInvalidParams.Err()
}
err := interceptor(nil, nil, &grpc.StreamServerInfo{FullMethod: "/test"}, handler)
assert.Error(t, err)
}
package interceptor
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
var unaryClientInvoker = func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
return nil
}
type streamClient struct {
}
func (s streamClient) Header() (metadata.MD, error) {
return metadata.MD{}, nil
}
func (s streamClient) Trailer() metadata.MD {
return metadata.MD{}
}
func (s streamClient) CloseSend() error {
return nil
}
func (s streamClient) Context() context.Context {
return context.Background()
}
func (s streamClient) SendMsg(m interface{}) error {
return nil
}
func (s streamClient) RecvMsg(m interface{}) error {
return nil
}
var streamClientFunc = func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return &streamClient{}, nil
}
// -----------------------------------------------------------------------------------------
var unaryServerInfo = &grpc.UnaryServerInfo{
Server: nil,
FullMethod: "/ping",
}
var unaryServerHandler = func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
}
func newStreamServer(ctx context.Context) *streamServer {
return &streamServer{
ctx: ctx,
}
}
type streamServer struct {
ctx context.Context
}
func (s streamServer) SetHeader(md metadata.MD) error {
return nil
}
func (s streamServer) SendHeader(md metadata.MD) error {
return nil
}
func (s streamServer) SetTrailer(md metadata.MD) {}
func (s streamServer) Context() context.Context {
return s.ctx
}
func (s streamServer) SendMsg(m interface{}) error {
return nil
}
func (s streamServer) RecvMsg(m interface{}) error {
return nil
}
var streamServerInfo = &grpc.StreamServerInfo{
FullMethod: "/test",
IsClientStream: false,
IsServerStream: false,
}
var streamServerHandler = func(srv interface{}, stream grpc.ServerStream) error {
return nil
}
package interceptor
import (
"context"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gitlab.wanzhuangkj.com/tush/xpkg/jwt"
)
// ---------------------------------- client ----------------------------------
// SetJwtTokenToCtx set the token (excluding prefix Bearer) to the context in grpc client side
// Example:
//
// authorization := "Bearer jwt-token"
//
// ctx := SetJwtTokenToCtx(ctx, authorization)
// cli.GetByID(ctx, req)
func SetJwtTokenToCtx(ctx context.Context, token string) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if ok {
md.Set(headerAuthorize, authScheme+" "+token)
} else {
md = metadata.Pairs(headerAuthorize, authScheme+" "+token)
}
return metadata.NewOutgoingContext(ctx, md)
}
// SetAuthToCtx set the authorization (including prefix Bearer) to the context in grpc client side
// Example:
//
// ctx := SetAuthToCtx(ctx, authorization)
// cli.GetByID(ctx, req)
func SetAuthToCtx(ctx context.Context, authorization string) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if ok {
md.Set(headerAuthorize, authorization)
} else {
md = metadata.Pairs(headerAuthorize, authorization)
}
return metadata.NewOutgoingContext(ctx, md)
}
// ---------------------------------- server interceptor ----------------------------------
var (
headerAuthorize = "authorization"
// auth Scheme
authScheme = "Bearer"
// authentication information in ctx key name
authCtxClaimsName = "tokenInfo"
// collection of skip authentication methods
authIgnoreMethods = map[string]struct{}{}
)
// GetAuthorization combining tokens into authentication information
func GetAuthorization(token string) string {
return authScheme + " " + token
}
// GetAuthCtxKey get the name of Claims
func GetAuthCtxKey() string {
return authCtxClaimsName
}
// StandardVerifyFn verify function, tokenTail32 is the last 32 characters of the token.
type StandardVerifyFn = func(claims *jwt.Claims, tokenTail32 string) error
// CustomVerifyFn verify custom function, tokenTail32 is the last 32 characters of the token.
type CustomVerifyFn = func(claims *jwt.CustomClaims, tokenTail32 string) error
type verifyOptions struct {
verifyType int // 1: use StandardVerifyFn, 2:use CustomVerifyFn
standardVerifyFn StandardVerifyFn
customVerifyFn CustomVerifyFn
}
func defaultVerifyOptions() *verifyOptions {
return &verifyOptions{
verifyType: 1,
}
}
// AuthOption setting the Authentication Field
type AuthOption func(*authOptions)
// authOptions settings
type authOptions struct {
authScheme string
ctxClaimsName string
ignoreMethods map[string]struct{}
verifyOpts *verifyOptions
}
func defaultAuthOptions() *authOptions {
return &authOptions{
authScheme: authScheme,
ctxClaimsName: authCtxClaimsName,
ignoreMethods: make(map[string]struct{}), // ways to ignore forensics
verifyOpts: defaultVerifyOptions(),
}
}
func (o *authOptions) apply(opts ...AuthOption) {
for _, opt := range opts {
opt(o)
}
}
// WithAuthScheme set the message prefix for authentication
func WithAuthScheme(scheme string) AuthOption {
return func(o *authOptions) {
o.authScheme = scheme
}
}
// WithAuthClaimsName set the key name of the information in ctx for authentication
func WithAuthClaimsName(claimsName string) AuthOption {
return func(o *authOptions) {
o.ctxClaimsName = claimsName
}
}
// WithAuthIgnoreMethods ways to ignore forensics
// fullMethodName format: /packageName.serviceName/methodName,
// example /api.userExample.v1.userExampleService/GetByID
func WithAuthIgnoreMethods(fullMethodNames ...string) AuthOption {
return func(o *authOptions) {
for _, method := range fullMethodNames {
o.ignoreMethods[method] = struct{}{}
}
}
}
// WithStandardVerify set the standard verify function for authentication
func WithStandardVerify(verify StandardVerifyFn) AuthOption {
return func(o *authOptions) {
if o.verifyOpts == nil {
o.verifyOpts = defaultVerifyOptions()
}
o.verifyOpts.verifyType = 1
o.verifyOpts.standardVerifyFn = verify
}
}
// WithCustomVerify set the custom verify function for authentication
func WithCustomVerify(verify CustomVerifyFn) AuthOption {
return func(o *authOptions) {
if o.verifyOpts == nil {
o.verifyOpts = defaultVerifyOptions()
}
o.verifyOpts.verifyType = 2
o.verifyOpts.customVerifyFn = verify
}
}
// -------------------------------------------------------------------------------------------
// verify authorization from context, support standard and custom verify processing
func jwtVerify(ctx context.Context, opt *verifyOptions) (context.Context, error) {
if opt == nil {
opt = &verifyOptions{
verifyType: 1, // default use VerifyGeneralFn
}
}
token, err := grpc_auth.AuthFromMD(ctx, authScheme) // key is authScheme
if err != nil {
return ctx, status.Errorf(codes.Unauthenticated, "%v", err)
}
if len(token) <= 100 {
return ctx, status.Errorf(codes.Unauthenticated, "authorization is illegal")
}
// custom claims
if opt.verifyType == 2 {
var claims *jwt.CustomClaims
claims, err = jwt.ParseCustomToken(token)
if err != nil {
return ctx, status.Errorf(codes.Unauthenticated, "%v", err)
}
if opt.customVerifyFn != nil {
tokenTail32 := token[len(token)-16:]
err = opt.customVerifyFn(claims, tokenTail32)
if err != nil {
return ctx, status.Errorf(codes.Unauthenticated, "%v", err)
}
}
newCtx := context.WithValue(ctx, authCtxClaimsName, claims) //nolint
return newCtx, nil
}
// standard claims
claims, err := jwt.ParseToken(token)
if err != nil {
return ctx, status.Errorf(codes.Unauthenticated, "%v", err)
}
if opt.standardVerifyFn != nil {
tokenTail32 := token[len(token)-16:]
err = opt.standardVerifyFn(claims, tokenTail32)
if err != nil {
return ctx, status.Errorf(codes.Unauthenticated, "%v", err)
}
}
newCtx := context.WithValue(ctx, authCtxClaimsName, claims) //nolint
return newCtx, nil
}
// GetJwtClaims get the jwt standard claims from context, contains fixed fields uid and name
func GetJwtClaims(ctx context.Context) (*jwt.Claims, bool) {
v, ok := ctx.Value(authCtxClaimsName).(*jwt.Claims)
return v, ok
}
// GetJwtCustomClaims get the jwt custom claims from context, contains custom fields
func GetJwtCustomClaims(ctx context.Context) (*jwt.CustomClaims, bool) {
v, ok := ctx.Value(authCtxClaimsName).(*jwt.CustomClaims)
return v, ok
}
// UnaryServerJwtAuth jwt unary interceptor
func UnaryServerJwtAuth(opts ...AuthOption) grpc.UnaryServerInterceptor {
o := defaultAuthOptions()
o.apply(opts...)
authScheme = o.authScheme
authCtxClaimsName = o.ctxClaimsName
authIgnoreMethods = o.ignoreMethods
verifyOpt := o.verifyOpts
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
var newCtx context.Context
var err error
if _, ok := authIgnoreMethods[info.FullMethod]; ok {
newCtx = ctx
} else {
newCtx, err = jwtVerify(ctx, verifyOpt)
if err != nil {
return nil, err
}
}
return handler(newCtx, req)
}
}
// StreamServerJwtAuth jwt stream interceptor
func StreamServerJwtAuth(opts ...AuthOption) grpc.StreamServerInterceptor {
o := defaultAuthOptions()
o.apply(opts...)
authScheme = o.authScheme
authCtxClaimsName = o.ctxClaimsName
authIgnoreMethods = o.ignoreMethods
verifyOpt := o.verifyOpts
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
var newCtx context.Context
var err error
if _, ok := authIgnoreMethods[info.FullMethod]; ok {
newCtx = stream.Context()
} else {
newCtx, err = jwtVerify(stream.Context(), verifyOpt)
if err != nil {
return err
}
}
wrapped := grpc_middleware.WrapServerStream(stream)
wrapped.WrappedContext = newCtx
return handler(srv, wrapped)
}
}
package interceptor
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gitlab.wanzhuangkj.com/tush/xpkg/jwt"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
var (
expectedUid = "100"
expectedName = "tom"
expectedFields = jwt.KV{"id": utils.StrToUint64(expectedUid), "name": expectedName, "age": 10}
)
func standardVerifyHandler(claims *jwt.Claims, tokenTail32 string) error {
// token := getToken(claims.UID)
// if token[len(token)-32:] != tokenTail32 { return err }
if claims.UID != expectedUid || claims.Name != expectedName {
return status.Error(codes.Unauthenticated, "id or name not match")
}
return nil
}
func customVerifyHandler(claims *jwt.CustomClaims, tokenTail32 string) error {
err := status.Error(codes.Unauthenticated, "custom verify failed")
//token, fields := getToken(id)
// if token[len(token)-32:] != tokenTail32 { return err }
id, exist := claims.GetUint64("id")
if !exist || id != expectedFields["id"] {
return err
}
name, exist := claims.GetString("name")
if !exist || name != expectedFields["name"] {
return err
}
age, exist := claims.GetInt("age")
if !exist || age != expectedFields["age"] {
return err
}
return nil
}
func TestJwtVerify(t *testing.T) {
jwt.Init()
ctx := context.Background()
token, _, _ := jwt.GenerateToken(expectedUid, expectedName)
// success test
ctx = metadata.NewIncomingContext(ctx, metadata.MD{headerAuthorize: []string{GetAuthorization(token)}})
newCtx, err := jwtVerify(ctx, nil)
assert.NoError(t, err)
claims, ok := GetJwtClaims(newCtx)
assert.True(t, ok)
assert.Equal(t, expectedUid, claims.UID)
// success test
ctx = metadata.NewIncomingContext(ctx, metadata.MD{headerAuthorize: []string{GetAuthorization(token)}})
newCtx, err = jwtVerify(ctx, &verifyOptions{verifyType: 1, standardVerifyFn: standardVerifyHandler})
assert.NoError(t, err)
claims, ok = GetJwtClaims(newCtx)
assert.True(t, ok)
assert.Equal(t, expectedUid, claims.UID)
authorization := []string{GetAuthorization("error token......")}
// authorization format error, missing token
ctx = metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: authorization})
_, err = jwtVerify(ctx, nil)
assert.Error(t, err)
// authorization format error, missing Bearer
ctx = context.WithValue(context.Background(), headerAuthorize, authorization)
_, err = jwtVerify(ctx, nil)
assert.Error(t, err)
}
func TestJwtCustomVerify(t *testing.T) {
jwt.Init()
ctx := context.Background()
token, _ := jwt.GenerateCustomToken(expectedFields)
verifyOpt := &verifyOptions{verifyType: 2}
// success test
ctx = metadata.NewIncomingContext(ctx, metadata.MD{headerAuthorize: []string{GetAuthorization(token)}})
newCtx, err := jwtVerify(ctx, verifyOpt)
assert.NoError(t, err)
claims, ok := GetJwtCustomClaims(newCtx)
assert.True(t, ok)
assert.Equal(t, expectedName, claims.Fields["name"])
// success test
ctx = metadata.NewIncomingContext(ctx, metadata.MD{headerAuthorize: []string{GetAuthorization(token)}})
verifyOpt.customVerifyFn = customVerifyHandler
newCtx, err = jwtVerify(ctx, verifyOpt)
assert.NoError(t, err)
claims, ok = GetJwtCustomClaims(newCtx)
assert.True(t, ok)
assert.Equal(t, expectedName, claims.Fields["name"])
authorization := []string{GetAuthorization("mock token......")}
// authorization format error, missing token
ctx = metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: authorization})
_, err = jwtVerify(ctx, verifyOpt)
assert.Error(t, err)
// authorization format error, missing Bearer
ctx = context.WithValue(context.Background(), headerAuthorize, authorization)
_, err = jwtVerify(ctx, verifyOpt)
assert.Error(t, err)
}
func TestUnaryServerJwtAuth(t *testing.T) {
interceptor := UnaryServerJwtAuth(WithStandardVerify(standardVerifyHandler))
assert.NotNil(t, interceptor)
// mock client ctx
jwt.Init()
token, _, _ := jwt.GenerateToken(expectedUid, expectedName)
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: []string{GetAuthorization(token)}})
_, err := interceptor(ctx, nil, unaryServerInfo, unaryServerHandler)
assert.NoError(t, err)
ctx = metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: []string{GetAuthorization("error token......")}})
_, err = interceptor(ctx, nil, unaryServerInfo, unaryServerHandler)
assert.Error(t, err)
}
func TestUnaryServerJwtCustomAuth(t *testing.T) {
interceptor := UnaryServerJwtAuth(WithCustomVerify(customVerifyHandler))
assert.NotNil(t, interceptor)
// mock client ctx
jwt.Init()
token, _ := jwt.GenerateCustomToken(expectedFields)
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: []string{GetAuthorization(token)}})
_, err := interceptor(ctx, nil, unaryServerInfo, unaryServerHandler)
assert.NoError(t, err)
ctx = metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: []string{GetAuthorization("error token......")}})
_, err = interceptor(ctx, nil, unaryServerInfo, unaryServerHandler)
assert.Error(t, err)
}
func TestStreamServerJwtAuth(t *testing.T) {
interceptor := StreamServerJwtAuth()
assert.NotNil(t, interceptor)
jwt.Init()
token, _, _ := jwt.GenerateToken(expectedUid, expectedName)
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: []string{authScheme + " " + token}})
err := interceptor(nil, newStreamServer(ctx), streamServerInfo, streamServerHandler)
assert.NoError(t, err)
err = interceptor(nil, newStreamServer(context.Background()), streamServerInfo, streamServerHandler)
assert.Error(t, err)
}
func TestStreamServerJwtCustomAuth(t *testing.T) {
interceptor := StreamServerJwtAuth(WithCustomVerify(nil))
assert.NotNil(t, interceptor)
jwt.Init()
token, _ := jwt.GenerateCustomToken(expectedFields)
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{headerAuthorize: []string{authScheme + " " + token}})
err := interceptor(nil, newStreamServer(ctx), streamServerInfo, streamServerHandler)
assert.NoError(t, err)
err = interceptor(nil, newStreamServer(context.Background()), streamServerInfo, streamServerHandler)
assert.Error(t, err)
}
func TestGetAuthCtxKey(t *testing.T) {
key := GetAuthCtxKey()
assert.Equal(t, authCtxClaimsName, key)
}
func TestGetAuthorization(t *testing.T) {
testData := "token"
authorization := GetAuthorization(testData)
assert.Equal(t, authScheme+" "+testData, authorization)
}
func TestAuthOptions(t *testing.T) {
o := defaultAuthOptions()
o.apply(WithAuthScheme(authScheme))
assert.Equal(t, authScheme, o.authScheme)
o.apply(WithAuthClaimsName(authCtxClaimsName))
assert.Equal(t, authCtxClaimsName, o.ctxClaimsName)
o.apply(WithAuthIgnoreMethods("/metrics"))
assert.Equal(t, struct{}{}, o.ignoreMethods["/metrics"])
o.apply(WithStandardVerify(nil))
assert.Equal(t, 1, o.verifyOpts.verifyType)
o.apply(WithStandardVerify(standardVerifyHandler))
assert.Equal(t, 1, o.verifyOpts.verifyType)
o.apply(WithCustomVerify(nil))
assert.Equal(t, 2, o.verifyOpts.verifyType)
o.apply(WithCustomVerify(customVerifyHandler))
assert.Equal(t, 2, o.verifyOpts.verifyType)
}
func TestSetJWTTokenToCtx(t *testing.T) {
jwt.Init()
ctx := context.Background()
token, _, _ := jwt.GenerateToken(expectedUid, expectedName)
expected := []string{GetAuthorization(token)}
ctx = SetJwtTokenToCtx(ctx, token)
md, _ := metadata.FromOutgoingContext(ctx)
assert.Equal(t, expected, md.Get(headerAuthorize))
}
func TestSetAuthToCtx(t *testing.T) {
jwt.Init()
ctx := context.Background()
token, _, _ := jwt.GenerateToken(expectedUid, expectedName)
authorization := GetAuthorization(token)
expected := []string{authorization}
ctx = SetAuthToCtx(ctx, authorization)
md, _ := metadata.FromOutgoingContext(ctx)
assert.Equal(t, expected, md.Get(headerAuthorize))
}
package interceptor
import (
"context"
"encoding/json"
"time"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
zapLog "gitlab.wanzhuangkj.com/tush/xpkg/logger"
)
var contentMark = []byte(" ...... ")
// ---------------------------------- client interceptor ----------------------------------
// UnaryClientLog client log unary interceptor
func UnaryClientLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryClientInterceptor {
o := defaultLogOptions()
o.apply(opts...)
if logger == nil {
logger, _ = zap.NewProduction()
}
if o.isReplaceGRPCLogger {
zapLog.ReplaceGRPCLoggerV2(logger)
}
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
startTime := time.Now()
var reqIDField zap.Field
if requestID := ClientCtxRequestID(ctx); requestID != "" {
reqIDField = zap.String(ContextRequestIDKey, requestID)
} else {
reqIDField = zap.Skip()
}
err := invoker(ctx, method, req, reply, cc, opts...)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.Error(err),
zap.String("type", "unary"),
zap.String("method", method),
zap.String("cost", time.Since(startTime).String()),
reqIDField,
}
logger.Info("invoker result", fields...)
return err
}
}
// StreamClientLog client log stream interceptor
func StreamClientLog(logger *zap.Logger, opts ...LogOption) grpc.StreamClientInterceptor {
o := defaultLogOptions()
o.apply(opts...)
if logger == nil {
logger, _ = zap.NewProduction()
}
if o.isReplaceGRPCLogger {
zapLog.ReplaceGRPCLoggerV2(logger)
}
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
startTime := time.Now()
var reqIDField zap.Field
if requestID := ClientCtxRequestID(ctx); requestID != "" {
reqIDField = zap.String(ContextRequestIDKey, requestID)
} else {
reqIDField = zap.Skip()
}
clientStream, err := streamer(ctx, desc, cc, method, opts...)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.Error(err),
zap.String("type", "stream"),
zap.String("method", method),
zap.String("cost", time.Since(startTime).String()),
reqIDField,
}
logger.Info("invoker result", fields...)
return clientStream, err
}
}
// ---------------------------------- server interceptor ----------------------------------
var defaultMaxLength = 300 // max length of response data to print
var ignoreLogMethods = map[string]struct{}{} // ignore printing methods
var defaultMarshalFn = func(reply interface{}) []byte {
data, _ := json.Marshal(reply)
return data
}
// LogOption log settings
type LogOption func(*logOptions)
type logOptions struct {
maxLength int
fields map[string]interface{}
ignoreMethods map[string]struct{}
isReplaceGRPCLogger bool
marshalFn func(reply interface{}) []byte // default json.Marshal
}
func defaultLogOptions() *logOptions {
return &logOptions{
maxLength: defaultMaxLength,
fields: make(map[string]interface{}),
ignoreMethods: make(map[string]struct{}),
marshalFn: defaultMarshalFn,
}
}
func (o *logOptions) apply(opts ...LogOption) {
for _, opt := range opts {
opt(o)
}
}
// WithMaxLen logger content max length
func WithMaxLen(maxLen int) LogOption {
return func(o *logOptions) {
if maxLen > 0 {
o.maxLength = maxLen
}
}
}
// WithReplaceGRPCLogger replace grpc logger v2
func WithReplaceGRPCLogger() LogOption {
return func(o *logOptions) {
o.isReplaceGRPCLogger = true
}
}
// WithLogFields adding a custom print field
func WithLogFields(kvs map[string]interface{}) LogOption {
return func(o *logOptions) {
if len(kvs) == 0 {
return
}
o.fields = kvs
}
}
// WithMarshalFn custom response data marshal function
func WithMarshalFn(fn func(reply interface{}) []byte) LogOption {
return func(o *logOptions) {
if fn != nil {
o.marshalFn = fn
}
}
}
// WithLogIgnoreMethods ignore printing methods
// fullMethodName format: /packageName.serviceName/methodName,
// example /api.userExample.v1.userExampleService/GetByID
func WithLogIgnoreMethods(fullMethodNames ...string) LogOption {
return func(o *logOptions) {
for _, method := range fullMethodNames {
o.ignoreMethods[method] = struct{}{}
}
}
}
// UnaryServerLog server-side log unary interceptor
func UnaryServerLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
}
if o.isReplaceGRPCLogger {
zapLog.ReplaceGRPCLoggerV2(logger)
}
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
return handler(ctx, req)
}
startTime := time.Now()
requestID := ServerCtxRequestID(ctx)
fields := []zap.Field{
zap.String("type", "unary"),
zap.String("method", info.FullMethod),
zap.Any("request", req),
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
}
logger.Info("<<<<<<<<<", fields...)
resp, err := handler(ctx, req)
data := o.marshalFn(resp)
if len(data) > o.maxLength {
data = append(data[:o.maxLength], contentMark...)
}
fields = []zap.Field{
zap.String("code", status.Code(err).String()),
zap.Error(err),
zap.String("type", "unary"),
zap.String("method", info.FullMethod),
zap.ByteString("data", data),
zap.String("cost", time.Since(startTime).String()),
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
}
logger.Info(">>>>>>>>>", fields...)
return resp, err
}
}
// UnaryServerSimpleLog server-side log unary interceptor, only print response
func UnaryServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
}
if o.isReplaceGRPCLogger {
zapLog.ReplaceGRPCLoggerV2(logger)
}
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
return handler(ctx, req)
}
startTime := time.Now()
requestID := ServerCtxRequestID(ctx)
resp, err := handler(ctx, req)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.Error(err),
zap.String("type", "unary"),
zap.String("method", info.FullMethod),
zap.String("cost", time.Since(startTime).String()),
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
}
logger.Info("[GRPC] response", fields...)
return resp, err
}
}
// StreamServerLog Server-side log stream interceptor
func StreamServerLog(logger *zap.Logger, opts ...LogOption) grpc.StreamServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
}
if o.isReplaceGRPCLogger {
zapLog.ReplaceGRPCLoggerV2(logger)
}
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
return handler(srv, stream)
}
startTime := time.Now()
requestID := ServerCtxRequestID(stream.Context())
fields := []zap.Field{
zap.String("type", "stream"),
zap.String("method", info.FullMethod),
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
}
logger.Info("<<<<<<<<<", fields...)
err := handler(srv, stream)
fields = []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("type", "stream"),
zap.String("method", info.FullMethod),
zap.String("cost", time.Since(startTime).String()),
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
}
logger.Info(">>>>>>>>>", fields...)
return err
}
}
// StreamServerSimpleLog Server-side log stream interceptor, only print response
func StreamServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.StreamServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
}
if o.isReplaceGRPCLogger {
zapLog.ReplaceGRPCLoggerV2(logger)
}
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
return handler(srv, stream)
}
startTime := time.Now()
requestID := ServerCtxRequestID(stream.Context())
err := handler(srv, stream)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("type", "stream"),
zap.String("method", info.FullMethod),
zap.String("cost", time.Since(startTime).String()),
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
}
logger.Info("[GRPC] response", fields...)
return err
}
}
package interceptor
import (
"encoding/json"
"testing"
"time"
"gitlab.wanzhuangkj.com/tush/xpkg/logger"
)
func TestUnaryClientLog(t *testing.T) {
addr := newUnaryRPCServer()
time.Sleep(time.Millisecond * 200)
cli := newUnaryRPCClient(addr,
UnaryClientRequestID(),
UnaryClientLog(logger.Get(), WithReplaceGRPCLogger()),
)
_ = sayHelloMethod(cli)
}
func TestUnaryServerLog(t *testing.T) {
addr := newUnaryRPCServer(
UnaryServerRequestID(),
UnaryServerLog(logger.Get(), WithReplaceGRPCLogger()),
UnaryServerSimpleLog(logger.Get(), WithReplaceGRPCLogger()),
)
time.Sleep(time.Millisecond * 200)
cli := newUnaryRPCClient(addr)
_ = sayHelloMethod(cli)
}
func TestStreamClientLog(t *testing.T) {
addr := newStreamRPCServer()
time.Sleep(time.Millisecond * 200)
cli := newStreamRPCClient(addr,
StreamClientRequestID(),
StreamClientLog(logger.Get(), WithReplaceGRPCLogger()),
)
_ = discussHelloMethod(cli)
time.Sleep(time.Millisecond)
}
func TestUnaryServerLog_ignore(t *testing.T) {
addr := newUnaryRPCServer(
UnaryServerLog(logger.Get(),
WithMaxLen(200),
WithLogFields(map[string]interface{}{"foo": "bar"}),
WithMarshalFn(func(reply interface{}) []byte {
data, _ := json.Marshal(reply)
return data
}),
WithLogIgnoreMethods("/api.user.v1.user/GetByID"),
),
)
time.Sleep(time.Millisecond * 200)
cli := newUnaryRPCClient(addr)
_ = sayHelloMethod(cli)
}
func TestStreamServerLog(t *testing.T) {
addr := newStreamRPCServer(
StreamServerRequestID(),
StreamServerLog(logger.Get(),
WithReplaceGRPCLogger(),
WithLogFields(map[string]interface{}{}),
),
StreamServerSimpleLog(logger.Get(),
WithReplaceGRPCLogger(),
WithLogFields(map[string]interface{}{}),
),
)
time.Sleep(time.Millisecond * 200)
cli := newStreamRPCClient(addr)
_ = discussHelloMethod(cli)
time.Sleep(time.Millisecond)
}
// ----------------------------------------------------------------------------------------
func TestNilLog(t *testing.T) {
UnaryClientLog(nil)
StreamClientLog(nil)
UnaryServerLog(nil)
UnaryServerSimpleLog(nil)
StreamServerLog(nil)
StreamServerSimpleLog(nil)
}
package interceptor
import (
"google.golang.org/grpc"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/metrics"
)
// UnaryClientMetrics client-side metrics unary interceptor
func UnaryClientMetrics() grpc.UnaryClientInterceptor {
return metrics.UnaryClientMetrics()
}
// StreamClientMetrics client-side metrics stream interceptor
func StreamClientMetrics() grpc.StreamClientInterceptor {
return metrics.StreamClientMetrics()
}
// UnaryServerMetrics server-side metrics unary interceptor
func UnaryServerMetrics(opts ...metrics.Option) grpc.UnaryServerInterceptor {
return metrics.UnaryServerMetrics(opts...)
}
// StreamServerMetrics server-side metrics stream interceptor
func StreamServerMetrics(opts ...metrics.Option) grpc.StreamServerInterceptor {
return metrics.StreamServerMetrics(opts...)
}
package interceptor
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStreamClientMetrics(t *testing.T) {
interceptor := StreamClientMetrics()
assert.NotNil(t, interceptor)
}
func TestStreamServerMetrics(t *testing.T) {
interceptor := StreamServerMetrics()
assert.NotNil(t, interceptor)
}
func TestUnaryClientMetrics(t *testing.T) {
interceptor := UnaryClientMetrics()
assert.NotNil(t, interceptor)
}
func TestUnaryServerMetrics(t *testing.T) {
interceptor := UnaryServerMetrics()
assert.NotNil(t, interceptor)
}
package interceptor
import (
"context"
"time"
"google.golang.org/grpc"
"gitlab.wanzhuangkj.com/tush/xpkg/errcode"
rl "gitlab.wanzhuangkj.com/tush/xpkg/shield/ratelimit"
)
// ---------------------------------- server interceptor ----------------------------------
// ErrLimitExceed is returned when the rate limiter is
// triggered and the request is rejected due to limit exceeded.
var ErrLimitExceed = rl.ErrLimitExceed
// RatelimitOption set the rate limits ratelimitOptions.
type RatelimitOption func(*ratelimitOptions)
type ratelimitOptions struct {
window time.Duration
bucket int
cpuThreshold int64
cpuQuota float64
}
func defaultRatelimitOptions() *ratelimitOptions {
return &ratelimitOptions{
window: time.Second * 10,
bucket: 100,
cpuThreshold: 800,
}
}
func (o *ratelimitOptions) apply(opts ...RatelimitOption) {
for _, opt := range opts {
opt(o)
}
}
// WithWindow with window size.
func WithWindow(d time.Duration) RatelimitOption {
return func(o *ratelimitOptions) {
o.window = d
}
}
// WithBucket with bucket size.
func WithBucket(b int) RatelimitOption {
return func(o *ratelimitOptions) {
o.bucket = b
}
}
// WithCPUThreshold with cpu threshold
func WithCPUThreshold(threshold int64) RatelimitOption {
return func(o *ratelimitOptions) {
o.cpuThreshold = threshold
}
}
// WithCPUQuota with real cpu quota(if it can not collect from process correct);
func WithCPUQuota(quota float64) RatelimitOption {
return func(o *ratelimitOptions) {
o.cpuQuota = quota
}
}
// UnaryServerRateLimit server-side unary circuit breaker interceptor
func UnaryServerRateLimit(opts ...RatelimitOption) grpc.UnaryServerInterceptor {
o := defaultRatelimitOptions()
o.apply(opts...)
limiter := rl.NewLimiter(
rl.WithWindow(o.window),
rl.WithBucket(o.bucket),
rl.WithCPUThreshold(o.cpuThreshold),
rl.WithCPUQuota(o.cpuQuota),
)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
done, err := limiter.Allow()
if err != nil {
return nil, errcode.StatusLimitExceed.ToRPCErr(err.Error())
}
reply, err := handler(ctx, req)
done(rl.DoneInfo{Err: err})
return reply, err
}
}
// StreamServerRateLimit server-side stream circuit breaker interceptor
func StreamServerRateLimit(opts ...RatelimitOption) grpc.StreamServerInterceptor {
o := defaultRatelimitOptions()
o.apply(opts...)
limiter := rl.NewLimiter(
rl.WithWindow(o.window),
rl.WithBucket(o.bucket),
rl.WithCPUThreshold(o.cpuThreshold),
rl.WithCPUQuota(o.cpuQuota),
)
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
done, err := limiter.Allow()
if err != nil {
return errcode.StatusLimitExceed.ToRPCErr(err.Error())
}
err = handler(srv, ss)
done(rl.DoneInfo{Err: err})
return err
}
}
package interceptor
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestUnaryServerRateLimit(t *testing.T) {
interceptor := UnaryServerRateLimit(
WithWindow(time.Second*10),
WithBucket(200),
WithCPUThreshold(500),
WithCPUQuota(0.5),
)
assert.NotNil(t, interceptor)
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
}
_, err := interceptor(nil, nil, nil, handler)
assert.NoError(t, err)
}
func TestStreamServerRateLimit(t *testing.T) {
interceptor := StreamServerRateLimit()
assert.NotNil(t, interceptor)
handler := func(srv interface{}, stream grpc.ServerStream) error {
return nil
}
err := interceptor(nil, nil, nil, handler)
assert.NoError(t, err)
}
package interceptor
import (
"context"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ---------------------------------- client interceptor ----------------------------------
// UnaryClientRecovery client-side unary recovery
func UnaryClientRecovery() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) {
defer func() {
if r := recover(); r != nil {
err = status.Errorf(codes.Internal, "triggered panic: %v", r)
}
}()
err = invoker(ctx, method, req, reply, cc, opts...)
return err
}
}
// StreamClientRecovery client-side recovery stream interceptor
func StreamClientRecovery() grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
streamer grpc.Streamer, opts ...grpc.CallOption) (s grpc.ClientStream, err error) {
defer func() {
if r := recover(); r != nil {
err = status.Errorf(codes.Internal, "triggered panic: %v", r)
}
}()
s, err = streamer(ctx, desc, cc, method, opts...)
return s, err
}
}
// ---------------------------------- server interceptor ----------------------------------
// UnaryServerRecovery recovery unary interceptor
func UnaryServerRecovery() grpc.UnaryServerInterceptor {
customFunc := func(p interface{}) (err error) {
return status.Errorf(codes.Internal, "triggered panic: %v", p)
}
opts := []grpc_recovery.Option{
grpc_recovery.WithRecoveryHandler(customFunc),
}
return grpc_recovery.UnaryServerInterceptor(opts...)
}
// StreamServerRecovery recovery stream interceptor
func StreamServerRecovery() grpc.StreamServerInterceptor {
customFunc := func(p interface{}) (err error) {
return status.Errorf(codes.Internal, "triggered panic: %v", p)
}
opts := []grpc_recovery.Option{
grpc_recovery.WithRecoveryHandler(customFunc),
}
return grpc_recovery.StreamServerInterceptor(opts...)
}
package interceptor
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnaryClientRecovery(t *testing.T) {
interceptor := UnaryClientRecovery()
assert.NotNil(t, interceptor)
err := interceptor(context.Background(), "/test", nil, nil, nil, unaryClientInvoker)
assert.NoError(t, err)
err = interceptor(context.Background(), "/test", nil, nil, nil, nil)
assert.NotNil(t, err)
}
func TestStreamClientRecovery(t *testing.T) {
interceptor := StreamClientRecovery()
assert.NotNil(t, interceptor)
_, err := interceptor(context.Background(), nil, nil, "/test", streamClientFunc)
assert.NoError(t, err)
_, err = interceptor(context.Background(), nil, nil, "/test", nil)
assert.NotNil(t, err)
}
func TestUnaryServerRecovery(t *testing.T) {
interceptor := UnaryServerRecovery()
assert.NotNil(t, interceptor)
_, err := interceptor(context.Background(), nil, unaryServerInfo, unaryServerHandler)
assert.NoError(t, err)
_, err = interceptor(context.Background(), nil, unaryServerInfo, nil)
assert.NotNil(t, err)
}
func TestStreamServerRecovery(t *testing.T) {
interceptor := StreamServerRecovery()
assert.NotNil(t, interceptor)
err := interceptor(nil, newStreamServer(context.Background()), streamServerInfo, streamServerHandler)
assert.NoError(t, err)
err = interceptor(nil, newStreamServer(context.Background()), streamServerInfo, nil)
assert.NotNil(t, err)
}
package interceptor
import (
"context"
ctxUtil "gitlab.wanzhuangkj.com/tush/xpkg/gin/ctxUtils"
"sync"
grpc_metadata "github.com/grpc-ecosystem/go-grpc-middleware/v2/metadata"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
var (
// ContextRequestIDKey request id key for context
ContextRequestIDKey = "request_id"
once sync.Once
)
// SetContextRequestIDKey set context request id key
func SetContextRequestIDKey(key string) {
if len(key) < 4 {
return
}
once.Do(func() {
ContextRequestIDKey = key
})
}
// CtxKeyString for context.WithValue key type
type CtxKeyString string
// RequestIDKey request_id
var RequestIDKey = CtxKeyString(ContextRequestIDKey)
// ---------------------------------- client interceptor ----------------------------------
// CtxRequestIDField get request id field from context.Context
func CtxRequestIDField(ctx context.Context) zap.Field {
return zap.String(ContextRequestIDKey, grpc_metadata.ExtractOutgoing(ctx).Get(ContextRequestIDKey))
}
// ClientCtxRequestID get request id from rpc client context.Context
func ClientCtxRequestID(ctx context.Context) string {
return grpc_metadata.ExtractOutgoing(ctx).Get(ContextRequestIDKey)
}
// ClientCtxRequestIDField get request id field from rpc client context.Context
func ClientCtxRequestIDField(ctx context.Context) zap.Field {
return zap.String(ContextRequestIDKey, grpc_metadata.ExtractOutgoing(ctx).Get(ContextRequestIDKey))
}
// UnaryClientRequestID client-side request_id unary interceptor
func UnaryClientRequestID() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
requestID := ClientCtxRequestID(ctx)
if requestID == "" {
requestID = ctxUtil.GenerateTid()
ctx = metadata.AppendToOutgoingContext(ctx, ContextRequestIDKey, requestID)
}
return invoker(ctx, method, req, reply, cc, opts...)
}
}
// StreamClientRequestID client request id stream interceptor
func StreamClientRequestID() grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
requestID := ClientCtxRequestID(ctx)
if requestID == "" {
requestID = ctxUtil.GenerateTid()
ctx = metadata.AppendToOutgoingContext(ctx, ContextRequestIDKey, requestID)
}
return streamer(ctx, desc, cc, method, opts...)
}
}
// ---------------------------------- server interceptor ----------------------------------
// KV key value
type KV struct {
Key string
Val interface{}
}
// WrapServerCtx wrap context, used in grpc server-side
func WrapServerCtx(ctx context.Context, kvs ...KV) context.Context {
ctx = context.WithValue(ctx, ContextRequestIDKey, grpc_metadata.ExtractIncoming(ctx).Get(ContextRequestIDKey)) //nolint
for _, kv := range kvs {
ctx = context.WithValue(ctx, kv.Key, kv.Val) //nolint
}
return ctx
}
// ServerCtxRequestID get request id from rpc server context.Context
func ServerCtxRequestID(ctx context.Context) string {
return grpc_metadata.ExtractIncoming(ctx).Get(ContextRequestIDKey)
}
// ServerCtxRequestIDField get request id field from rpc server context.Context
func ServerCtxRequestIDField(ctx context.Context) zap.Field {
return zap.String(ContextRequestIDKey, grpc_metadata.ExtractIncoming(ctx).Get(ContextRequestIDKey))
}
// UnaryServerRequestID server-side request_id unary interceptor
func UnaryServerRequestID() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
requestID := ServerCtxRequestID(ctx)
if requestID == "" {
requestID = ctxUtil.GenerateTid()
ctx = grpc_metadata.ExtractIncoming(ctx).Add(ContextRequestIDKey, requestID).ToIncoming(ctx)
}
return handler(ctx, req)
}
}
// StreamServerRequestID server-side request id stream interceptor
func StreamServerRequestID() grpc.StreamServerInterceptor {
// todo
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
//ctx := stream.Context()
//requestID := ServerCtxRequestID(ctx)
//if requestID == "" {
// requestID = ctxUtils.GenerateTid()
// ctx = grpc_metadata.ExtractIncoming(ctx).Add(ContextRequestIDKey, requestID).ToIncoming(ctx)
//}
return handler(srv, stream)
}
}
package interceptor
import (
"context"
"fmt"
"io"
"net"
"reflect"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/runtime/protoimpl"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func newUnaryRPCServer(unaryServerInterceptors ...grpc.UnaryServerInterceptor) string {
return newRPCServer(unaryServerInterceptors, nil)
}
func newStreamRPCServer(streamServerInterceptors ...grpc.StreamServerInterceptor) string {
return newRPCServer(nil, streamServerInterceptors)
}
func newRPCServer(unaryServerInterceptors []grpc.UnaryServerInterceptor, streamServerInterceptors []grpc.StreamServerInterceptor) string {
serverAddr, _ := utils.GetLocalHTTPAddrPairs()
list, err := net.Listen("tcp", serverAddr)
if err != nil {
panic(err)
}
options1 := grpc.ChainUnaryInterceptor(unaryServerInterceptors...)
options2 := grpc.ChainStreamInterceptor(streamServerInterceptors...)
server := grpc.NewServer(options1, options2)
RegisterGreeterServer(server, &greeterServer{})
go func() {
err = server.Serve(list)
if err != nil {
panic(err)
}
}()
return serverAddr
}
func newUnaryRPCClient(addr string, unaryClientInterceptors ...grpc.UnaryClientInterceptor) GreeterClient {
return newRPCClient(addr, unaryClientInterceptors, nil)
}
func newStreamRPCClient(addr string, streamClientInterceptors ...grpc.StreamClientInterceptor) GreeterClient {
return newRPCClient(addr, nil, streamClientInterceptors)
}
func newRPCClient(addr string, unaryClientInterceptors []grpc.UnaryClientInterceptor, streamClientInterceptors []grpc.StreamClientInterceptor) GreeterClient {
var options []grpc.DialOption
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
option1 := grpc.WithChainUnaryInterceptor(unaryClientInterceptors...)
option2 := grpc.WithChainStreamInterceptor(streamClientInterceptors...)
options = append(options, option1, option2)
addr = "127.0.0.1" + addr
conn, err := grpc.NewClient(addr, options...)
if err != nil {
panic(err)
}
return NewGreeterClient(conn)
}
func sayHelloMethod(client GreeterClient) error {
resp, err := client.SayHello(context.Background(), &HelloRequest{Name: "foo"})
if err != nil {
return err
}
fmt.Println("resp:", resp.Message)
return nil
}
func discussHelloMethod(client GreeterClient) error {
stream, err := client.DiscussHello(context.Background())
if err != nil {
return err
}
names := []string{"foo1", "foo2"}
var resp *HelloReply
for _, name := range names {
err = stream.Send(&HelloRequest{Name: name})
if err != nil {
return err
}
resp, err = stream.Recv()
if err != nil {
if err == io.EOF {
break
}
return err
}
fmt.Println("client receive:", resp.Message)
}
time.Sleep(10 * time.Millisecond)
err = stream.CloseSend()
if err != nil {
return err
}
return nil
}
type greeterServer struct {
UnimplementedGreeterServer
}
func (g *greeterServer) SayHello(ctx context.Context, r *HelloRequest) (*HelloReply, error) {
return &HelloReply{Message: "hello " + r.Name}, nil
}
func (g *greeterServer) DiscussHello(stream Greeter_DiscussHelloServer) error {
recValues := []string{}
sendValues := []string{}
defer func() {
fmt.Println("\nserver receive: ", recValues)
fmt.Println("server send : ", sendValues)
}()
var resp *HelloRequest
var err error
for {
resp, err = stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
recValues = append(recValues, resp.Name)
sendMsg := "hello " + resp.Name
err = stream.Send(&HelloReply{Message: sendMsg})
if err != nil {
return err
}
sendValues = append(sendValues, sendMsg)
}
}
// -----------------------------------hello.pb.go-------------------------------------------
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HelloRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *HelloRequest) Reset() {
*x = HelloRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_hello_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HelloRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloRequest) ProtoMessage() {}
func (x *HelloRequest) ProtoReflect() protoreflect.Message {
mi := &file_hello_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
func (*HelloRequest) Descriptor() ([]byte, []int) {
return file_hello_proto_rawDescGZIP(), []int{0}
}
func (x *HelloRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type HelloReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *HelloReply) Reset() {
*x = HelloReply{}
if protoimpl.UnsafeEnabled {
mi := &file_hello_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HelloReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloReply) ProtoMessage() {}
func (x *HelloReply) ProtoReflect() protoreflect.Message {
mi := &file_hello_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.
func (*HelloReply) Descriptor() ([]byte, []int) {
return file_hello_proto_rawDescGZIP(), []int{1}
}
func (x *HelloReply) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_hello_proto protoreflect.FileDescriptor
var file_hello_proto_rawDesc = []byte{
0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c,
0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x32, 0x7d, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x08, 0x53,
0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22,
0x00, 0x12, 0x3c, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x63, 0x75, 0x73, 0x73, 0x48, 0x65, 0x6c, 0x6c,
0x6f, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48,
0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42,
0x0b, 0x5a, 0x09, 0x2e, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
file_hello_proto_rawDescOnce sync.Once
file_hello_proto_rawDescData = file_hello_proto_rawDesc
)
func file_hello_proto_rawDescGZIP() []byte {
file_hello_proto_rawDescOnce.Do(func() {
file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData)
})
return file_hello_proto_rawDescData
}
var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_hello_proto_goTypes = []interface{}{
(*HelloRequest)(nil), // 0: proto.HelloRequest
(*HelloReply)(nil), // 1: proto.HelloReply
}
var file_hello_proto_depIdxs = []int32{
0, // 0: proto.Greeter.SayHello:input_type -> proto.HelloRequest
0, // 1: proto.Greeter.DiscussHello:input_type -> proto.HelloRequest
1, // 2: proto.Greeter.SayHello:output_type -> proto.HelloReply
1, // 3: proto.Greeter.DiscussHello:output_type -> proto.HelloReply
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_hello_proto_init() }
func file_hello_proto_init() {
if File_hello_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HelloRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_hello_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HelloReply); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_hello_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_hello_proto_goTypes,
DependencyIndexes: file_hello_proto_depIdxs,
MessageInfos: file_hello_proto_msgTypes,
}.Build()
File_hello_proto = out.File
file_hello_proto_rawDesc = nil
file_hello_proto_goTypes = nil
file_hello_proto_depIdxs = nil
}
// -----------------------------------hello_grpc.pb.go-------------------------------------
const _ = grpc.SupportPackageIsVersion7
// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type GreeterClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
DiscussHello(ctx context.Context, opts ...grpc.CallOption) (Greeter_DiscussHelloClient, error)
}
type greeterClient struct {
cc grpc.ClientConnInterface
}
func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := c.cc.Invoke(ctx, "/proto.Greeter/SayHello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *greeterClient) DiscussHello(ctx context.Context, opts ...grpc.CallOption) (Greeter_DiscussHelloClient, error) {
stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[0], "/proto.Greeter/DiscussHello", opts...)
if err != nil {
return nil, err
}
x := &greeterDiscussHelloClient{stream}
return x, nil
}
type Greeter_DiscussHelloClient interface {
Send(*HelloRequest) error
Recv() (*HelloReply, error)
grpc.ClientStream
}
type greeterDiscussHelloClient struct {
grpc.ClientStream
}
func (x *greeterDiscussHelloClient) Send(m *HelloRequest) error {
return x.ClientStream.SendMsg(m)
}
func (x *greeterDiscussHelloClient) Recv() (*HelloReply, error) {
m := new(HelloReply)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// GreeterServer is the server API for Greeter service.
// All implementations must embed UnimplementedGreeterServer
// for forward compatibility
type GreeterServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
DiscussHello(Greeter_DiscussHelloServer) error
mustEmbedUnimplementedGreeterServer()
}
// UnimplementedGreeterServer must be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct {
}
func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedGreeterServer) DiscussHello(Greeter_DiscussHelloServer) error {
return status.Errorf(codes.Unimplemented, "method DiscussHello not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to GreeterServer will
// result in compilation errors.
type UnsafeGreeterServer interface {
mustEmbedUnimplementedGreeterServer()
}
func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
s.RegisterService(&Greeter_ServiceDesc, srv)
}
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.Greeter/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Greeter_DiscussHello_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(GreeterServer).DiscussHello(&greeterDiscussHelloServer{stream})
}
type Greeter_DiscussHelloServer interface {
Send(*HelloReply) error
Recv() (*HelloRequest, error)
grpc.ServerStream
}
type greeterDiscussHelloServer struct {
grpc.ServerStream
}
func (x *greeterDiscussHelloServer) Send(m *HelloReply) error {
return x.ServerStream.SendMsg(m)
}
func (x *greeterDiscussHelloServer) Recv() (*HelloRequest, error) {
m := new(HelloRequest)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Greeter_ServiceDesc = grpc.ServiceDesc{
ServiceName: "proto.Greeter",
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "DiscussHello",
Handler: _Greeter_DiscussHello_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "hello.proto",
}
// ------------------------------------------------------------------------------------------
func TestUnaryClientRequestID(t *testing.T) {
addr := newUnaryRPCServer()
time.Sleep(time.Millisecond * 200)
cli := newUnaryRPCClient(addr, UnaryClientRequestID())
_ = sayHelloMethod(cli)
}
func TestUnaryServerRequestID(t *testing.T) {
addr := newUnaryRPCServer(UnaryServerRequestID())
time.Sleep(time.Millisecond * 200)
cli := newUnaryRPCClient(addr)
_ = sayHelloMethod(cli)
}
func TestStreamClientRequestID(t *testing.T) {
addr := newStreamRPCServer()
time.Sleep(time.Millisecond * 200)
cli := newStreamRPCClient(addr, StreamClientRequestID())
_ = discussHelloMethod(cli)
time.Sleep(time.Millisecond)
}
func TestStreamServerRequestID(t *testing.T) {
addr := newStreamRPCServer(StreamServerRequestID())
time.Sleep(time.Millisecond * 200)
cli := newStreamRPCClient(addr)
_ = discussHelloMethod(cli)
time.Sleep(time.Millisecond)
}
func TestCtxRequestID(t *testing.T) {
_ = ClientCtxRequestID(context.Background())
field := CtxRequestIDField(context.Background())
assert.NotNil(t, field)
field = ClientCtxRequestIDField(context.Background())
assert.NotNil(t, field)
ctx := WrapServerCtx(context.Background())
assert.NotNil(t, ctx)
ctx = WrapServerCtx(context.Background(), KV{Key: "foo", Val: "bar"})
assert.NotNil(t, ctx)
_ = ServerCtxRequestID(context.Background())
field = ServerCtxRequestIDField(context.Background())
assert.NotNil(t, field)
}
func TestSetContextRequestIDKey(t *testing.T) {
SetContextRequestIDKey("my_request_id")
SetContextRequestIDKey("foo_bar") // invalid key, sync.Once
t.Log(ContextRequestIDKey)
SetContextRequestIDKey("xx") // invalid key
}
package interceptor
import (
"context"
"time"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// ---------------------------------- client interceptor ----------------------------------
var (
// default error code for triggering a retry
defaultErrCodes = []codes.Code{codes.Internal}
)
// RetryOption set the retry retryOptions.
type RetryOption func(*retryOptions)
type retryOptions struct {
times uint
interval time.Duration
errCodes []codes.Code
}
func defaultRetryOptions() *retryOptions {
return &retryOptions{
times: 2, // default retry times
interval: time.Millisecond * 100, // default retry interval 100 ms
errCodes: defaultErrCodes, // default error code for triggering a retry
}
}
func (o *retryOptions) apply(opts ...RetryOption) {
for _, opt := range opts {
opt(o)
}
}
// WithRetryTimes set number of retries, max 10
func WithRetryTimes(n uint) RetryOption {
return func(o *retryOptions) {
if n > 10 {
n = 10
}
o.times = n
}
}
// WithRetryInterval set the retry interval from 1 ms to 10 seconds
func WithRetryInterval(t time.Duration) RetryOption {
return func(o *retryOptions) {
if t < time.Millisecond {
t = time.Millisecond
} else if t > 10*time.Second {
t = 10 * time.Second
}
o.interval = t
}
}
// WithRetryErrCodes set the trigger retry error code
func WithRetryErrCodes(errCodes ...codes.Code) RetryOption {
for _, errCode := range errCodes {
switch errCode {
case codes.Internal, codes.DeadlineExceeded, codes.Unavailable:
default:
defaultErrCodes = append(defaultErrCodes, errCode)
}
}
return func(o *retryOptions) {
o.errCodes = defaultErrCodes
}
}
// UnaryClientRetry client-side retry unary interceptor
func UnaryClientRetry(opts ...RetryOption) grpc.UnaryClientInterceptor {
o := defaultRetryOptions()
o.apply(opts...)
return grpc_retry.UnaryClientInterceptor(
grpc_retry.WithMax(o.times), // set the number of retries
grpc_retry.WithBackoff(func(ctx context.Context, attempt uint) time.Duration { // set retry interval
return o.interval
}),
grpc_retry.WithCodes(o.errCodes...), // set retry error code
)
}
// StreamClientRetry client-side retry stream interceptor
func StreamClientRetry(opts ...RetryOption) grpc.StreamClientInterceptor {
o := defaultRetryOptions()
o.apply(opts...)
return grpc_retry.StreamClientInterceptor(
grpc_retry.WithMax(o.times), // set the number of retries
grpc_retry.WithBackoff(func(ctx context.Context, attempt uint) time.Duration { // set retry interval
return o.interval
}),
grpc_retry.WithCodes(o.errCodes...), // set retry error code
)
}
package interceptor
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
)
func TestStreamClientRetry(t *testing.T) {
interceptor := StreamClientRetry()
assert.NotNil(t, interceptor)
}
func TestUnaryClientRetry(t *testing.T) {
interceptor := UnaryClientRetry()
assert.NotNil(t, interceptor)
}
func TestWithRetryErrCodes(t *testing.T) {
testData := codes.Canceled
opt := WithRetryErrCodes(testData)
o := new(retryOptions)
o.apply(opt)
assert.Contains(t, o.errCodes, testData)
}
func TestWithRetryInterval(t *testing.T) {
testData := time.Second
opt := WithRetryInterval(testData)
o := new(retryOptions)
o.apply(opt)
assert.Equal(t, testData, o.interval)
testData = time.Microsecond
opt = WithRetryInterval(testData)
o = new(retryOptions)
o.apply(opt)
assert.Equal(t, true, o.interval == time.Millisecond)
testData = time.Minute
opt = WithRetryInterval(testData)
o = new(retryOptions)
o.apply(opt)
assert.Equal(t, true, o.interval == 10*time.Second)
}
func TestWithRetryTimes(t *testing.T) {
testData := uint(5)
opt := WithRetryTimes(testData)
o := new(retryOptions)
o.apply(opt)
assert.Equal(t, testData, o.times)
testData = uint(20)
opt = WithRetryTimes(testData)
o = new(retryOptions)
o.apply(opt)
assert.NotEqual(t, testData, o.times)
}
func Test_defaultRetryOptions(t *testing.T) {
o := defaultRetryOptions()
assert.NotNil(t, o)
}
func Test_retryOptions_apply(t *testing.T) {
testData := uint(5)
opt := WithRetryTimes(testData)
o := new(retryOptions)
o.apply(opt)
assert.Equal(t, testData, o.times)
}
package interceptor
import (
"context"
"time"
"google.golang.org/grpc"
)
// ---------------------------------- client interceptor ----------------------------------
var timeoutVal = time.Second * 10
// UnaryClientTimeout client-side timeout unary interceptor
func UnaryClientTimeout(d time.Duration) grpc.UnaryClientInterceptor {
if d < time.Millisecond {
d = timeoutVal
}
return func(ctx context.Context, method string, req, resp interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx, _ = context.WithTimeout(ctx, d) //nolint
return invoker(ctx, method, req, resp, cc, opts...)
}
}
// StreamClientTimeout server-side timeout interceptor
func StreamClientTimeout(d time.Duration) grpc.StreamClientInterceptor {
if d < time.Millisecond {
d = timeoutVal
}
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
ctx, _ = context.WithTimeout(ctx, d) //nolint
return streamer(ctx, desc, cc, method, opts...)
}
}
package interceptor
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestUnaryClientTimeout(t *testing.T) {
interceptor := UnaryClientTimeout(time.Millisecond)
assert.NotNil(t, interceptor)
interceptor = UnaryClientTimeout(time.Second)
assert.NotNil(t, interceptor)
err := interceptor(context.Background(), "/test", nil, nil, nil, unaryClientInvoker)
assert.NoError(t, err)
}
func TestStreamClientTimeout(t *testing.T) {
interceptor := StreamClientTimeout(time.Millisecond)
assert.NotNil(t, interceptor)
interceptor = StreamClientTimeout(time.Second)
assert.NotNil(t, interceptor)
_, err := interceptor(context.Background(), nil, nil, "/test", streamClientFunc)
assert.NoError(t, err)
}
package interceptor
import (
"context"
grpc_metadata "github.com/grpc-ecosystem/go-grpc-middleware/v2/metadata"
"google.golang.org/grpc"
)
// ---------------------------------- client option ----------------------------------
type authToken struct {
AppID string `json:"app_id"`
AppKey string `json:"app_key"`
IsSecure bool `json:"isSecure"`
}
// GetRequestMetadata get metadata
func (t *authToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { //nolint
return map[string]string{
"app_id": t.AppID,
"app_key": t.AppKey,
}, nil
}
// RequireTransportSecurity is require transport secure
func (t *authToken) RequireTransportSecurity() bool {
return t.IsSecure
}
// ClientTokenOption client token
func ClientTokenOption(appID string, appKey string, isSecure bool) grpc.DialOption {
return grpc.WithPerRPCCredentials(&authToken{appID, appKey, isSecure})
}
// ---------------------------------- server interceptor ----------------------------------
// CheckToken check app id and app key
// Example:
//
// var f CheckToken=func(appID string, appKey string) error{
// if appID != targetAppID || appKey != targetAppKey {
// return status.Errorf(codes.Unauthenticated, "app id or app key checksum failure")
// }
// return nil
// }
type CheckToken func(appID string, appKey string) error
// UnaryServerToken recovery unary token
func UnaryServerToken(f CheckToken) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
appID := grpc_metadata.ExtractIncoming(ctx).Get("app_id")
appKey := grpc_metadata.ExtractIncoming(ctx).Get("app_key")
err := f(appID, appKey)
if err != nil {
return nil, err
}
return handler(ctx, req)
}
}
// StreamServerToken recovery stream token
func StreamServerToken(f CheckToken) grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := stream.Context()
appID := grpc_metadata.ExtractIncoming(ctx).Get("app_id")
appKey := grpc_metadata.ExtractIncoming(ctx).Get("app_key")
err := f(appID, appKey)
if err != nil {
return err
}
return handler(srv, stream)
}
}
package interceptor
import (
"context"
"testing"
grpc_metadata "github.com/grpc-ecosystem/go-grpc-middleware/v2/metadata"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestToken(t *testing.T) {
to := &authToken{
AppID: "grpc",
AppKey: "123456",
IsSecure: false,
}
metadata, err := to.GetRequestMetadata(context.Background())
assert.NoError(t, err)
assert.NotNil(t, metadata)
isSecure := to.RequireTransportSecurity()
assert.Equal(t, to.IsSecure, isSecure)
}
func TestClientTokenOption(t *testing.T) {
option := ClientTokenOption("grpc", "123456", false)
assert.NotNil(t, option)
}
func TestUnaryServerToken(t *testing.T) {
f := func(appID string, appKey string) error {
if appID != "grpc" || appKey != "123456" {
return status.Errorf(codes.Unauthenticated, "app id or app key checksum failure")
}
return nil
}
interceptor := UnaryServerToken(f)
assert.NotNil(t, interceptor)
ctx := context.Background()
_, err := interceptor(ctx, nil, unaryServerInfo, unaryServerHandler)
assert.NotNil(t, err)
ctx = grpc_metadata.ExtractIncoming(ctx).Add("app_id", "grpc").ToIncoming(ctx)
ctx = grpc_metadata.ExtractIncoming(ctx).Add("app_key", "123456").ToIncoming(ctx)
_, err = interceptor(ctx, nil, unaryServerInfo, unaryServerHandler)
assert.NoError(t, err)
}
func TestStreamServerToken(t *testing.T) {
f := func(appID string, appKey string) error {
if appID != "grpc" || appKey != "123456" {
return status.Errorf(codes.Unauthenticated, "app id or app key checksum failure")
}
return nil
}
interceptor := StreamServerToken(f)
assert.NotNil(t, interceptor)
ctx := context.Background()
err := interceptor(nil, newStreamServer(ctx), streamServerInfo, streamServerHandler)
assert.NotNil(t, err)
ctx = grpc_metadata.ExtractIncoming(ctx).Add("app_id", "grpc").ToIncoming(ctx)
ctx = grpc_metadata.ExtractIncoming(ctx).Add("app_key", "123456").ToIncoming(ctx)
err = interceptor(nil, newStreamServer(ctx), streamServerInfo, streamServerHandler)
assert.NoError(t, err)
}
package interceptor
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
// UnaryClientTracing client-side tracing unary interceptor
func UnaryClientTracing() grpc.UnaryClientInterceptor {
return otelgrpc.UnaryClientInterceptor() //nolint
}
// StreamClientTracing client-side tracing stream interceptor
func StreamClientTracing() grpc.StreamClientInterceptor {
return otelgrpc.StreamClientInterceptor() //nolint
}
// UnaryServerTracing server-side tracing unary interceptor
func UnaryServerTracing() grpc.UnaryServerInterceptor {
return otelgrpc.UnaryServerInterceptor() //nolint
}
// StreamServerTracing server-side tracing stream interceptor
func StreamServerTracing() grpc.StreamServerInterceptor {
return otelgrpc.StreamServerInterceptor() //nolint
}
// ClientOptionTracing client-side tracing interceptor
func ClientOptionTracing() grpc.DialOption {
return grpc.WithStatsHandler(otelgrpc.NewClientHandler())
}
// ServerOptionTracing server-side tracing interceptor
func ServerOptionTracing() grpc.ServerOption {
return grpc.StatsHandler(otelgrpc.NewServerHandler())
}
package interceptor
import (
"google.golang.org/grpc"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStreamClientTracing(t *testing.T) {
interceptor := StreamClientTracing()
assert.NotNil(t, interceptor)
}
func TestStreamServerTracing(t *testing.T) {
interceptor := StreamServerTracing()
assert.NotNil(t, interceptor)
}
func TestUnaryClientTracing(t *testing.T) {
interceptor := UnaryClientTracing()
assert.NotNil(t, interceptor)
}
func TestUnaryServerTracing(t *testing.T) {
interceptor := UnaryServerTracing()
assert.NotNil(t, interceptor)
}
func TestClientOptionTracing(t *testing.T) {
_, _ = grpc.NewClient("localhost", ClientOptionTracing())
}
func TestServerOptionTracing(t *testing.T) {
_ = grpc.NewServer(ServerOptionTracing())
}
// Package keepalive is setting grpc keepalive parameters.
package keepalive
import (
"math"
"time"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc"
)
// ---------------------------------- client option ----------------------------------
var kacp = keepalive.ClientParameters{
Time: 20 * time.Second, // send pings every 10 seconds if there is no activity
Timeout: 1 * time.Second, // wait 1 second for ping ack before considering the connection dead
PermitWithoutStream: true, // send pings even without active streams
}
// ClientKeepAlive keep the connection set
func ClientKeepAlive() grpc.DialOption {
return grpc.WithKeepaliveParams(kacp)
}
// ---------------------------------- server option ----------------------------------
const (
infinity = time.Duration(math.MaxInt64)
defaultMaxConnectionIdle = infinity
defaultMaxConnectionAge = infinity
defaultMaxConnectionAgeGrace = infinity
)
var kaep = keepalive.EnforcementPolicy{
MinTime: 5 * time.Second, // If a client pings more than once every 5 seconds, terminate the connection
PermitWithoutStream: true, // Allow pings even when there are no active streams
}
var kasp = keepalive.ServerParameters{
MaxConnectionIdle: defaultMaxConnectionIdle, // If a client is idle for 15 seconds, send a GOAWAY
MaxConnectionAge: defaultMaxConnectionAge, // If any connection is alive for more than 30 seconds, send a GOAWAY
MaxConnectionAgeGrace: defaultMaxConnectionAgeGrace, // Allow 5 seconds for pending RPCs to complete before forcibly closing connections
Time: 20 * time.Second, // Ping the client if it is idle for 5 seconds to ensure the connection is still active
Timeout: 1 * time.Second, // Wait 1 second for the ping ack before assuming the connection is dead
}
// ServerKeepAlive keep the connection set
func ServerKeepAlive() []grpc.ServerOption {
return []grpc.ServerOption{
grpc.KeepaliveEnforcementPolicy(kaep),
grpc.KeepaliveParams(kasp),
}
}
package keepalive
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestClientKeepAlive(t *testing.T) {
alive := ClientKeepAlive()
assert.NotNil(t, alive)
}
func TestServerKeepAlive(t *testing.T) {
alives := ServerKeepAlive()
assert.Equal(t, 2, len(alives))
}
package metrics
import (
"net/http"
"sync"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/grpc"
)
var (
// client side default router
clientPattern = "/rpc_client/metrics"
// create a Registry
cliReg = prometheus.NewRegistry()
// initialize the client's default metrics
grpcClientMetrics = grpc_prometheus.NewClientMetrics()
cliOnce sync.Once
)
func cliRegisterMetrics() {
cliOnce.Do(func() {
// register metrics, including custom metrics
cliReg.MustRegister(grpcClientMetrics)
})
}
// SetClientPattern set the client pattern
func SetClientPattern(pattern string) {
if pattern != "" {
clientPattern = pattern
}
}
// ClientRegister for http routing and grpc methods
func ClientRegister(mux *http.ServeMux) {
// register for http routing
mux.Handle(clientPattern, promhttp.HandlerFor(cliReg, promhttp.HandlerOpts{}))
}
// ClientHTTPService initialize the client's prometheus exporter service and use http://ip:port/metrics to fetch data
func ClientHTTPService(addr string) *http.Server {
httpServer := &http.Server{
Addr: addr,
Handler: promhttp.HandlerFor(cliReg, promhttp.HandlerOpts{}),
}
// run http server
go func() {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic("listen and serve error: " + err.Error())
}
}()
return httpServer
}
// ---------------------------------- client interceptor ----------------------------------
// UnaryClientMetrics metrics unary interceptor
func UnaryClientMetrics() grpc.UnaryClientInterceptor {
cliRegisterMetrics()
return grpcClientMetrics.UnaryClientInterceptor()
}
// StreamClientMetrics metrics stream interceptor
func StreamClientMetrics() grpc.StreamClientInterceptor {
cliRegisterMetrics()
return grpcClientMetrics.StreamClientInterceptor()
}
package metrics
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func TestClientHTTPService(t *testing.T) {
serverAddr, _ := utils.GetLocalHTTPAddrPairs()
s := ClientHTTPService(serverAddr)
ctx, _ := context.WithTimeout(context.Background(), time.Second)
time.Sleep(time.Millisecond * 100)
err := s.Shutdown(ctx)
assert.NoError(t, err)
}
func TestStreamClientMetrics(t *testing.T) {
metrics := StreamClientMetrics()
assert.NotNil(t, metrics)
}
func TestUnaryClientMetrics(t *testing.T) {
metrics := UnaryClientMetrics()
assert.NotNil(t, metrics)
}
func Test_cliRegisterMetrics(t *testing.T) {
cliRegisterMetrics()
}
func TestClientRegister(t *testing.T) {
SetClientPattern("/rpc_client/metrics")
ClientRegister(http.NewServeMux())
}
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Common visualisations of gRPC Client metrics",
"editable": true,
"gnetId": null,
"graphTooltip": 1,
"id": 28,
"iteration": 1670407425372,
"links": [],
"panels": [
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"panels": [],
"title": "Requests",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "Rate of gRPC requests started",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "opm"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 1
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_client_started_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method)",
"legendFormat": "{{grpc_service}}.{{grpc_method}}",
"range": true,
"refId": "A"
}
],
"title": "Requests started [1m]",
"type": "timeseries"
},
{
"datasource": "Prometheus",
"description": "Rate of requests completed, by status",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "opm"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 9
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_client_handled_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method, grpc_code)",
"legendFormat": "{{grpc_service}}.{{grpc_method}} - {{grpc_code}}",
"range": true,
"refId": "A"
}
],
"title": "Requests completed [1m]",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 17
},
"id": 11,
"panels": [],
"title": "Errors",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "Rate of errors, by status",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "opm"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 18
},
"id": 7,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_client_handled_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_code!=\"OK\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method, grpc_code)",
"legendFormat": "{{grpc_service}}.{{grpc_method}} - {{grpc_code}}",
"range": true,
"refId": "A"
}
],
"title": "Request errors [1m]",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 26
},
"id": 13,
"panels": [],
"title": "Latency",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 27
},
"id": 9,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "histogram_quantile(0.99, \n sum(rate(grpc_client_handling_seconds_bucket{grpc_type=\"unary\", grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=\"unary\"}[5m])) by (grpc_service, grpc_method, le)\n)",
"legendFormat": "{{grpc_service}}.{{grpc_method}}",
"range": true,
"refId": "A"
}
],
"title": "99th percentile response latency [5m]",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 35
},
"id": 15,
"panels": [],
"title": "Messaging",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "Rate of messages received by server",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 36
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_client_msg_received_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Server messages received [1m]",
"type": "timeseries"
},
{
"datasource": "Prometheus",
"description": "Rate of messages sent by server",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 36
},
"id": 18,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_client_msg_sent_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Server messages sent [1m]",
"type": "timeseries"
}
],
"refresh": false,
"schemaVersion": 27,
"style": "dark",
"tags": [
"grpc",
"client"
],
"templating": {
"list": [
{
"allValue": ".*",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": "Prometheus",
"definition": "label_values(grpc_client_started_total, grpc_service)",
"description": "gRPC Server Name",
"error": null,
"hide": 0,
"includeAll": true,
"label": "Service",
"multi": true,
"name": "service",
"options": [],
"query": {
"query": "label_values(grpc_client_started_total, grpc_service)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": ".*",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": "Prometheus",
"definition": "label_values(grpc_client_started_total{grpc_service=~\"$service\"}, grpc_method)",
"description": "gRPC Method Name",
"error": null,
"hide": 0,
"includeAll": true,
"label": "Method",
"multi": true,
"name": "method",
"options": [],
"query": {
"query": "label_values(grpc_client_started_total{grpc_service=~\"$service\"}, grpc_method)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": ".*",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": "Prometheus",
"definition": "label_values(grpc_client_started_total{grpc_service=~\"$service\", grpc_method=~\"$method\"}, grpc_type)",
"description": "gRPC request type - bidirectional stream, server stream, unary, client_stream",
"error": null,
"hide": 0,
"includeAll": true,
"label": "Type",
"multi": true,
"name": "type",
"options": [],
"query": {
"query": "label_values(grpc_client_started_total{grpc_service=~\"$service\", grpc_method=~\"$method\"}, grpc_type)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-12h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"title": "gRPC / Client",
"uid": "dxEjpXn4z",
"version": 1
}
package metrics
import (
"net"
"sync"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
)
// ConnectionOption set connection option
type ConnectionOption func(*connectionOptions)
type connectionOptions struct {
zapLogger *zap.Logger
connectionGauge prometheus.Gauge
}
func defaultConnectionOptions() *connectionOptions {
return &connectionOptions{}
}
func (o *connectionOptions) apply(opts ...ConnectionOption) {
for _, opt := range opts {
opt(o)
}
}
// WithConnectionsLogger set logger for connection
func WithConnectionsLogger(l *zap.Logger) ConnectionOption {
return func(o *connectionOptions) {
if l != nil {
o.zapLogger = l
}
}
}
// WithConnectionsGauge set prometheus gauge for connections
func WithConnectionsGauge() ConnectionOption {
return func(o *connectionOptions) {
o.connectionGauge = grpcConnectionGauge
}
}
// ------------------------------------------------------------------------------------------
// CustomConn custom connections, intercept disconnected behavior
type CustomConn struct {
net.Conn
listener *CustomListener
}
// CustomListener custom listener for counting connections
type CustomListener struct {
net.Listener
activeConnections int
mu sync.Mutex
zapLogger *zap.Logger
connectionGauge prometheus.Gauge
}
// Accept waits for and returns the next connection to the listener.
func (l *CustomListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
var count int
l.mu.Lock()
l.activeConnections++
count = l.activeConnections
l.mu.Unlock()
if l.zapLogger != nil {
l.zapLogger.Info("new grpc client connected", zap.String("client", conn.RemoteAddr().String()), zap.Int("active connections", count))
}
if l.connectionGauge != nil {
l.connectionGauge.Set(float64(count))
}
return &CustomConn{
Conn: conn,
listener: l,
}, nil
}
// GetActiveConnections returns the number of active connections.
func (l *CustomListener) GetActiveConnections() int {
l.mu.Lock()
defer l.mu.Unlock()
return l.activeConnections
}
// closes the connection and decrements the active connections count.
func (l *CustomListener) closeConnection(clientAddr string) {
var count int
l.mu.Lock()
l.activeConnections--
count = l.activeConnections
l.mu.Unlock()
if l.zapLogger != nil {
l.zapLogger.Info("grpc client disconnected", zap.String("client", clientAddr), zap.Int("active connections", count))
}
if l.connectionGauge != nil {
l.connectionGauge.Set(float64(count))
}
}
// Close closes the listener, any blocked except operations will be unblocked and return errors.
func (c *CustomConn) Close() error {
defer func() { _ = recover() }()
clientAddr := c.Conn.RemoteAddr().String()
err := c.Conn.Close()
if err == nil {
c.listener.closeConnection(clientAddr)
}
return err
}
// NewCustomListener creates a new custom listener.
func NewCustomListener(listener net.Listener, opts ...ConnectionOption) *CustomListener {
o := defaultConnectionOptions()
o.apply(opts...)
return &CustomListener{
Listener: listener,
zapLogger: o.zapLogger,
connectionGauge: o.connectionGauge,
}
}
// Package metrics is grpc's server-side and client-side metrics can continue to be captured using prometheus.
package metrics
import (
"net/http"
"sync"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/grpc"
)
// https://github.com/grpc-ecosystem/go-grpc-prometheus/tree/master/examples/grpc-server-with-prometheus
var (
// server side default router
serverPattern = "/metrics"
// create a Registry
srvReg = prometheus.NewRegistry()
// initialize server-side default metrics
grpcServerMetrics = grpc_prometheus.NewServerMetrics()
// go metrics
goMetrics = collectors.NewGoCollector()
// user-defined metrics https://prometheus.io/docs/concepts/metric_types/#histogram
customizedCounterMetrics = []*prometheus.CounterVec{}
customizedSummaryMetrics = []*prometheus.SummaryVec{}
customizedGaugeMetrics = []*prometheus.GaugeVec{}
customizedHistogramMetrics = []*prometheus.HistogramVec{}
grpcConnectionGauge = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "grpc_server_active_connections",
Help: "Current number of active gRPC client connections",
},
)
srvOnce sync.Once
)
// Option set metrics
type Option func(*options)
type options struct{}
func defaultMetricsOptions() *options {
return &options{}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithCounterMetrics add Counter type indicator
func WithCounterMetrics(metrics ...*prometheus.CounterVec) Option {
return func(o *options) {
customizedCounterMetrics = append(customizedCounterMetrics, metrics...)
}
}
// WithSummaryMetrics add Summary type indicator
func WithSummaryMetrics(metrics ...*prometheus.SummaryVec) Option {
return func(o *options) {
customizedSummaryMetrics = append(customizedSummaryMetrics, metrics...)
}
}
// WithGaugeMetrics add Gauge type indicator
func WithGaugeMetrics(metrics ...*prometheus.GaugeVec) Option {
return func(o *options) {
customizedGaugeMetrics = append(customizedGaugeMetrics, metrics...)
}
}
// WithHistogramMetrics adding Histogram type indicators
func WithHistogramMetrics(metrics ...*prometheus.HistogramVec) Option {
return func(o *options) {
customizedHistogramMetrics = append(customizedHistogramMetrics, metrics...)
}
}
func srvRegisterMetrics() {
srvOnce.Do(func() {
// enable time record
grpcServerMetrics.EnableHandlingTimeHistogram()
// register go metrics
srvReg.MustRegister(goMetrics)
// register metrics to capture, custom metrics also need to be registered
srvReg.MustRegister(grpcServerMetrics)
// register custom Counter metrics
for _, metric := range customizedCounterMetrics {
srvReg.MustRegister(metric)
}
for _, metric := range customizedSummaryMetrics {
srvReg.MustRegister(metric)
}
for _, metric := range customizedGaugeMetrics {
srvReg.MustRegister(metric)
}
for _, metric := range customizedHistogramMetrics {
srvReg.MustRegister(metric)
}
srvReg.MustRegister(grpcConnectionGauge)
})
}
// SetServerPattern set the server pattern
func SetServerPattern(pattern string) {
if pattern != "" {
serverPattern = pattern
}
}
// Register for http routing and grpc methods
func Register(mux *http.ServeMux, grpcServer *grpc.Server) {
// register for http routing
mux.Handle(serverPattern, promhttp.HandlerFor(srvReg, promhttp.HandlerOpts{}))
// register all gRPC methods to metrics
grpcServerMetrics.InitializeMetrics(grpcServer)
}
// ServerHTTPService initialize the prometheus exporter service on the server side and fetch data using http://ip:port/metrics
func ServerHTTPService(addr string, grpcServer *grpc.Server) *http.Server {
httpServer := &http.Server{
Addr: addr,
Handler: promhttp.HandlerFor(srvReg, promhttp.HandlerOpts{}),
}
// run http server
go func() {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic("listen and serve error: " + err.Error())
}
}()
// initialize gRPC methods Metrics
grpcServerMetrics.InitializeMetrics(grpcServer)
return httpServer
}
// ---------------------------------- server interceptor ----------------------------------
// UnaryServerMetrics metrics unary interceptor
func UnaryServerMetrics(opts ...Option) grpc.UnaryServerInterceptor {
o := defaultMetricsOptions()
o.apply(opts...)
srvRegisterMetrics()
return grpcServerMetrics.UnaryServerInterceptor()
}
// StreamServerMetrics metrics stream interceptor
func StreamServerMetrics(opts ...Option) grpc.StreamServerInterceptor {
o := defaultMetricsOptions()
o.apply(opts...)
srvRegisterMetrics()
return grpcServerMetrics.StreamServerInterceptor()
}
package metrics
import (
"context"
"net/http"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func Test_srvRegisterMetrics(t *testing.T) {
opts := []Option{
WithCounterMetrics(prometheus.NewCounterVec(prometheus.CounterOpts{Name: "demo1"}, []string{})),
WithGaugeMetrics(prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: "demo2"}, []string{})),
WithHistogramMetrics(prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "demo3"}, []string{})),
WithSummaryMetrics(prometheus.NewSummaryVec(prometheus.SummaryOpts{Name: "demo4"}, []string{})),
}
o := defaultMetricsOptions()
o.apply(opts...)
srvRegisterMetrics()
}
func TestWithCounterMetrics(t *testing.T) {
testData := &prometheus.CounterVec{}
opt := WithCounterMetrics(testData)
o := new(options)
o.apply(opt)
assert.Contains(t, customizedCounterMetrics, testData)
}
func TestWithGaugeMetrics(t *testing.T) {
testData := &prometheus.GaugeVec{}
opt := WithGaugeMetrics(testData)
o := new(options)
o.apply(opt)
assert.Contains(t, customizedGaugeMetrics, testData)
}
func TestWithHistogramMetrics(t *testing.T) {
testData := &prometheus.HistogramVec{}
opt := WithHistogramMetrics(testData)
o := new(options)
o.apply(opt)
assert.Contains(t, customizedHistogramMetrics, testData)
}
func TestWithSummaryMetrics(t *testing.T) {
testData := &prometheus.SummaryVec{}
opt := WithSummaryMetrics(testData)
o := new(options)
o.apply(opt)
assert.Contains(t, customizedSummaryMetrics, testData)
}
func Test_defaultMetricsOptions(t *testing.T) {
o := defaultMetricsOptions()
assert.NotNil(t, o)
}
func Test_metricsOptions_apply(t *testing.T) {
testData := &prometheus.SummaryVec{}
opt := WithSummaryMetrics(testData)
o := defaultMetricsOptions()
o.apply(opt)
assert.Contains(t, customizedSummaryMetrics, testData)
}
func TestRegister(t *testing.T) {
SetServerPattern("/rpc_server/metrics")
Register(http.NewServeMux(), grpc.NewServer())
}
func TestServerHTTPService(t *testing.T) {
serverAddr, _ := utils.GetLocalHTTPAddrPairs()
s := ServerHTTPService(serverAddr, grpc.NewServer())
ctx, _ := context.WithTimeout(context.Background(), time.Second)
time.Sleep(time.Millisecond * 100)
err := s.Shutdown(ctx)
assert.NoError(t, err)
}
func TestStreamServerMetrics(t *testing.T) {
metrics := StreamServerMetrics()
assert.NotNil(t, metrics)
}
func TestUnaryServerMetrics(t *testing.T) {
metrics := UnaryServerMetrics()
assert.NotNil(t, metrics)
}
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Common visualisations of gRPC (Golang) Server metrics",
"editable": true,
"gnetId": null,
"graphTooltip": 1,
"id": 27,
"iteration": 1670405362978,
"links": [],
"panels": [
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"panels": [],
"title": "Requests",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "Rate of gRPC requests started",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "opm"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 1
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_server_started_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method)",
"legendFormat": "{{grpc_service}}.{{grpc_method}}",
"range": true,
"refId": "A"
}
],
"title": "Requests started [1m]",
"type": "timeseries"
},
{
"datasource": "Prometheus",
"description": "Rate of requests completed, by status",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "opm"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 9
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_server_handled_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method, grpc_code)",
"legendFormat": "{{grpc_service}}.{{grpc_method}} - {{grpc_code}}",
"range": true,
"refId": "A"
}
],
"title": "Requests completed [1m]",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 17
},
"id": 11,
"panels": [],
"title": "Errors",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "Rate of errors, by status",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "opm"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 18
},
"id": 7,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_server_handled_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_code!=\"OK\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method, grpc_code)",
"legendFormat": "{{grpc_service}}.{{grpc_method}} - {{grpc_code}}",
"range": true,
"refId": "A"
}
],
"title": "Request errors [1m]",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 26
},
"id": 13,
"panels": [],
"title": "Latency",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 27
},
"id": 9,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "histogram_quantile(0.99, \n sum(rate(grpc_server_handling_seconds_bucket{grpc_type=\"unary\", grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=\"unary\"}[5m])) by (grpc_service, grpc_method, le)\n)",
"legendFormat": "{{grpc_service}}.{{grpc_method}}",
"range": true,
"refId": "A"
}
],
"title": "99th percentile response latency [5m]",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 35
},
"id": 15,
"panels": [],
"title": "Messaging",
"type": "row"
},
{
"datasource": "Prometheus",
"description": "Rate of messages received by server",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 36
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_server_msg_received_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Server messages received [1m]",
"type": "timeseries"
},
{
"datasource": "Prometheus",
"description": "Rate of messages sent by server",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {},
"thresholdsStyle": {}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 36
},
"id": 18,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
},
"tooltipOptions": {
"mode": "single"
}
},
"targets": [
{
"datasource": "Prometheus",
"editorMode": "code",
"expr": "sum(increase(grpc_server_msg_sent_total{grpc_service=~\"$service\", grpc_method=~\"$method\", grpc_type=~\"$type\"}[1m])) by (grpc_service, grpc_method)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Server messages sent [1m]",
"type": "timeseries"
}
],
"schemaVersion": 27,
"style": "dark",
"tags": [
"grpc",
"server"
],
"templating": {
"list": [
{
"allValue": ".*",
"current": {
"selected": true,
"tags": [],
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": "Prometheus",
"definition": "label_values(grpc_server_started_total, grpc_service)",
"description": "gRPC Server Name",
"error": null,
"hide": 0,
"includeAll": true,
"label": "Service",
"multi": true,
"name": "service",
"options": [],
"query": {
"query": "label_values(grpc_server_started_total, grpc_service)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": ".*",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": "Prometheus",
"definition": "label_values(grpc_server_started_total{grpc_service=~\"$service\"}, grpc_method)",
"description": "gRPC Method Name",
"error": null,
"hide": 0,
"includeAll": true,
"label": "Method",
"multi": true,
"name": "method",
"options": [],
"query": {
"query": "label_values(grpc_server_started_total{grpc_service=~\"$service\"}, grpc_method)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": ".*",
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": "Prometheus",
"definition": "label_values(grpc_server_started_total{grpc_service=~\"$service\", grpc_method=~\"$method\"}, grpc_type)",
"description": "gRPC request type - bidirectional stream, server stream, unary, client_stream",
"error": null,
"hide": 0,
"includeAll": true,
"label": "Type",
"multi": true,
"name": "type",
"options": [],
"query": {
"query": "label_values(grpc_server_started_total{grpc_service=~\"$service\", grpc_method=~\"$method\"}, grpc_type)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"title": "gRPC / Server",
"uid": "fDH4_QMVk",
"version": 1
}
// Package resolve is setting grpc client-side load balancing policy.
package resolve
import (
"fmt"
"net/url"
"sync"
"google.golang.org/grpc/resolver"
)
var mutex = &sync.Mutex{}
// Register address and serviceName
func Register(scheme string, serviceName string, address []string) string {
mutex.Lock()
defer mutex.Unlock()
endpoint := fmt.Sprintf("%s:///%s", scheme, serviceName)
u, _ := url.Parse(endpoint)
resolver.Register(&ResolverBuilder{
scheme: scheme,
serviceName: serviceName,
addrs: address,
path: u.Path,
})
return endpoint
}
// ResolverBuilder resolver struct
type ResolverBuilder struct {
scheme string
serviceName string
addrs []string
path string
}
// Build resolver
func (r *ResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) {
blr := &blResolver{
target: target,
cc: cc,
addrsStore: map[string][]string{
r.path: r.addrs,
},
}
blr.start()
return blr, nil
}
// Scheme get scheme
func (r *ResolverBuilder) Scheme() string {
return r.scheme
}
type blResolver struct {
target resolver.Target
cc resolver.ClientConn
addrsStore map[string][]string
}
func (b *blResolver) start() {
addrStrs := b.addrsStore[b.target.URL.Path]
addrs := make([]resolver.Address, len(addrStrs))
for i, s := range addrStrs {
addrs[i] = resolver.Address{Addr: s}
}
_ = b.cc.UpdateState(resolver.State{Addresses: addrs})
}
// ResolveNow Resolve now
func (*blResolver) ResolveNow(_ resolver.ResolveNowOptions) {}
// Close resolver
func (*blResolver) Close() {}
package resolve
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
)
var r = &ResolverBuilder{
scheme: "grpc",
serviceName: "demo",
addrs: []string{"localhost:8282"},
}
func TestRegister(t *testing.T) {
s := Register(r.scheme, r.serviceName, r.addrs)
assert.Equal(t, true, strings.Contains(s, r.serviceName))
}
func TestResolverBuilder_Build(t *testing.T) {
c := &clientConn{}
_, err := r.Build(resolver.Target{}, c, resolver.BuildOptions{})
assert.NoError(t, err)
}
func TestResolverBuilder_Scheme(t *testing.T) {
str := r.Scheme()
assert.NotEmpty(t, str)
}
func Test_blResolver_Close(t *testing.T) {
c := &clientConn{}
b, err := r.Build(resolver.Target{}, c, resolver.BuildOptions{})
assert.NoError(t, err)
b.Close()
}
func Test_blResolver_ResolveNow(t *testing.T) {
c := &clientConn{}
b, err := r.Build(resolver.Target{}, c, resolver.BuildOptions{})
assert.NoError(t, err)
b.ResolveNow(struct{}{})
}
func Test_blResolver_start(t *testing.T) {
b := &blResolver{
target: resolver.Target{},
cc: &clientConn{},
addrsStore: make(map[string][]string),
}
b.start()
}
type clientConn struct{}
func (c clientConn) UpdateState(state resolver.State) error { return nil }
func (c clientConn) ReportError(err error) {}
func (c clientConn) NewAddress(addresses []resolver.Address) {}
func (c clientConn) NewServiceConfig(serviceConfig string) {}
func (c clientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
return &serviceconfig.ParseResult{}
}
// Package server is generic grpc server-side.
package server
import (
"fmt"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/metrics"
)
// RegisterFn register object
type RegisterFn func(srv *grpc.Server)
// ServiceRegisterFn service register
type ServiceRegisterFn func()
// Option set server option
type Option func(*options)
type options struct {
credentials credentials.TransportCredentials
unaryInterceptors []grpc.UnaryServerInterceptor
streamInterceptors []grpc.StreamServerInterceptor
serviceRegisterFn ServiceRegisterFn
isShowConnections bool
connectionOptions []metrics.ConnectionOption
}
func defaultServerOptions() *options {
return &options{}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithSecure set secure
func WithSecure(credential credentials.TransportCredentials) Option {
return func(o *options) {
o.credentials = credential
}
}
// WithUnaryInterceptor set unary interceptor
func WithUnaryInterceptor(interceptors ...grpc.UnaryServerInterceptor) Option {
return func(o *options) {
o.unaryInterceptors = interceptors
}
}
// WithStreamInterceptor set stream interceptor
func WithStreamInterceptor(interceptors ...grpc.StreamServerInterceptor) Option {
return func(o *options) {
o.streamInterceptors = interceptors
}
}
// WithServiceRegister set service register
func WithServiceRegister(fn ServiceRegisterFn) Option {
return func(o *options) {
o.serviceRegisterFn = fn
}
}
// WithStatConnections enable stat connections
func WithStatConnections(opts ...metrics.ConnectionOption) Option {
return func(o *options) {
o.isShowConnections = true
o.connectionOptions = opts
}
}
func customInterceptorOptions(o *options) []grpc.ServerOption {
var opts []grpc.ServerOption
if o.credentials != nil {
opts = append(opts, grpc.Creds(o.credentials))
}
if len(o.unaryInterceptors) > 0 {
option := grpc.ChainUnaryInterceptor(o.unaryInterceptors...)
opts = append(opts, option)
}
if len(o.streamInterceptors) > 0 {
option := grpc.ChainStreamInterceptor(o.streamInterceptors...)
opts = append(opts, option)
}
return opts
}
// Run grpc server
func Run(port int, registerFn RegisterFn, options ...Option) {
o := defaultServerOptions()
o.apply(options...)
// listening on TCP port
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
panic(err)
}
if o.isShowConnections {
listener = metrics.NewCustomListener(listener, o.connectionOptions...)
}
// create a grpc server where interceptors can be injected
srv := grpc.NewServer(customInterceptorOptions(o)...)
// register object to the server
registerFn(srv)
// register service to target
if o.serviceRegisterFn != nil {
o.serviceRegisterFn()
}
go func() {
// run the server
err = srv.Serve(listener)
if err != nil {
panic(err)
}
}()
}
package server
import (
"context"
"fmt"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/metrics"
"gitlab.wanzhuangkj.com/tush/xpkg/logger"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
var fn = func(s *grpc.Server) {
// pb.RegisterGreeterServer(s, &greeterServer{})
}
var unaryInterceptors = []grpc.UnaryServerInterceptor{
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
return nil, nil
},
}
var streamInterceptors = []grpc.StreamServerInterceptor{
func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return nil
},
}
func TestRun(t *testing.T) {
port, _ := utils.GetAvailablePort()
Run(port, fn,
WithSecure(insecure.NewCredentials()),
WithUnaryInterceptor(unaryInterceptors...),
WithStreamInterceptor(streamInterceptors...),
WithServiceRegister(func() {}),
WithStatConnections(metrics.WithConnectionsLogger(logger.Get()), metrics.WithConnectionsGauge()),
)
t.Log("grpc server started", port)
time.Sleep(time.Second * 2)
conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", port), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Error(err)
return
}
time.Sleep(time.Second * 2)
_ = conn.Close()
time.Sleep(time.Second * 1)
}
// Package jy2struct is a library for generating go struct code, supporting json and yaml.
package jy2struct
import (
"bytes"
"errors"
"os"
"strings"
)
// Args convert arguments
type Args struct {
Format string // document format, json or yaml
Data string // json or yaml content
InputFile string // file
Name string // name of structure
SubStruct bool // are sub-structures separated
Tags string // add additional tags, multiple tags separated by commas
tags []string //nolint
convertFloats bool
parser Parser
}
func (j *Args) checkValid() error {
switch j.Format {
case "json":
j.parser = ParseJSON
j.convertFloats = true
case "yaml":
j.parser = ParseYaml
default:
return errors.New("format must be json or yaml")
}
j.tags = []string{j.Format}
tags := strings.Split(j.Tags, ",")
for _, tag := range tags {
if tag == j.Format || tag == "" {
continue
}
j.tags = append(j.tags, tag)
}
if j.Name == "" {
j.Name = "GenerateName"
}
return nil
}
// Convert json or yaml to go struct
func Convert(args *Args) (string, error) {
err := args.checkValid()
if err != nil {
return "", err
}
var data []byte
if args.Data != "" {
data = []byte(args.Data)
} else {
data, err = os.ReadFile(args.InputFile)
if err != nil {
return "", err
}
}
input := bytes.NewReader(data)
output, err := jyParse(input, args.parser, args.Name, "main", args.tags, args.SubStruct, args.convertFloats)
if err != nil {
return "", err
}
return string(output), nil
}
package jy2struct
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConvert(t *testing.T) {
type args struct {
args *Args
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "json to struct from data",
args: args{args: &Args{
Data: `{"name":"foo","age":11}`,
Format: "json",
}},
wantErr: false,
},
{
name: "yaml to struct from data",
args: args{args: &Args{
Data: `name: "foo"
age: 10`,
Format: "yaml",
}},
wantErr: false,
},
{
name: "json to struct from file",
args: args{args: &Args{
InputFile: "test.json",
Format: "json",
SubStruct: true,
Tags: "gorm",
}},
wantErr: false,
},
{
name: "yaml to struct from file",
args: args{args: &Args{
InputFile: "test.yaml",
Format: "yaml",
SubStruct: true,
}},
wantErr: false,
},
{
name: "json to slice from data",
args: args{args: &Args{
Data: `[{"name":"foo","age":11},{"name":"foo2","age":22}]`,
Format: "json",
}},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Convert(tt.args.args)
if (err != nil) != tt.wantErr {
t.Errorf("Convert() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Log(got)
})
}
// test Convert error
arg := &Args{Format: "unknown"}
_, err := Convert(arg)
assert.Error(t, err)
arg = &Args{Format: "yaml", InputFile: "notfound.yaml"}
_, err = Convert(arg)
assert.Error(t, err)
}
package jy2struct
import (
"bytes"
"encoding/json"
"fmt"
"go/format"
"io"
"math"
"reflect"
"sort"
"strconv"
"strings"
"unicode"
"gopkg.in/yaml.v3"
)
// ForceFloats whether to force a change to float
var ForceFloats bool
// commonInitialisms is a set of common initialisms.
// Only add entries that are highly unlikely to be non-initialisms.
// For instance, "ID" is fine (Freudian code is rare), but "AND" is not.
var commonInitialisms = map[string]bool{
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"GUID": true,
"HTML": true,
"HTTP": true,
"HTTPS": true,
"ID": true,
"IP": true,
"JSON": true,
"LHS": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SSH": true,
"TLS": true,
"TTL": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"NTP": true,
"DB": true,
}
var intToWordMap = []string{
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
}
// Parser parser function
type Parser func(io.Reader) (interface{}, error)
// ParseJSON parse json to struct
func ParseJSON(input io.Reader) (interface{}, error) {
var result interface{}
if err := json.NewDecoder(input).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
// ParseYaml parse yaml to struct
func ParseYaml(input io.Reader) (interface{}, error) {
var result interface{}
b, err := readFile(input)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(b, &result); err != nil {
return nil, err
}
return result, nil
}
func readFile(input io.Reader) ([]byte, error) {
buf := bytes.NewBuffer(nil)
_, err := io.Copy(buf, input)
if err != nil {
return []byte{}, nil
}
return buf.Bytes(), nil
}
// json or yaml parse
func jyParse(input io.Reader, parser Parser, structName, pkgName string, tags []string, subStruct bool, convertFloats bool) ([]byte, error) {
_ = pkgName
var subStructMap map[string]string
if subStruct {
subStructMap = make(map[string]string)
}
var result map[string]interface{}
iresult, err := parser(input)
if err != nil {
return nil, err
}
switch iresult := iresult.(type) {
case map[interface{}]interface{}:
result = convertKeysToStrings(iresult)
case map[string]interface{}:
result = iresult
case []interface{}:
src := fmt.Sprintf("\ntype %s %s\n", structName, typeForValue(iresult, structName, tags, subStructMap, convertFloats))
// supplementary sub-structures
for k, v := range subStructMap {
src += fmt.Sprintf("\n\ntype %s %s\n\n", v, k)
}
var formatted []byte
formatted, err = format.Source([]byte(src))
if err != nil {
err = fmt.Errorf("error formatting: %s, was formatting\n%s", err, src)
}
return formatted, err
default:
return nil, fmt.Errorf("unexpected type: %T", iresult)
}
src := fmt.Sprintf("\ntype %s %s}", structName, generateTypes(result, structName, tags, 0, subStructMap, convertFloats))
keys := make([]string, 0, len(subStructMap))
for key := range subStructMap {
keys = append(keys, key)
}
sort.Strings(keys)
for _, k := range keys {
src = fmt.Sprintf("%v\n\ntype %v %v", src, subStructMap[k], k)
}
formatted, err := format.Source([]byte(src))
if err != nil {
err = fmt.Errorf("error formatting: %s, was formatting\n%s", err, src)
}
return formatted, err
}
func convertKeysToStrings(obj map[interface{}]interface{}) map[string]interface{} {
res := make(map[string]interface{})
for k, v := range obj {
res[fmt.Sprintf("%v", k)] = v
}
return res
}
// jyParse go struct entries for a map[string]interface{} structure
func generateTypes(obj map[string]interface{}, structName string, tags []string, depth int, subStructMap map[string]string, convertFloats bool) string {
structure := "struct {"
keys := make([]string, 0, len(obj))
for key := range obj {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
value := obj[key]
valueType := typeForValue(value, structName, tags, subStructMap, convertFloats)
//value = mergeElements(value)
//If a nested value, recurse
switch value := value.(type) {
case []interface{}:
if len(value) > 0 {
sub := ""
if v, ok := value[0].(map[interface{}]interface{}); ok {
sub = generateTypes(convertKeysToStrings(v), structName, tags, depth+1, subStructMap, convertFloats) + "}"
} else if v, ok := value[0].(map[string]interface{}); ok {
sub = generateTypes(v, structName, tags, depth+1, subStructMap, convertFloats) + "}"
}
if sub != "" {
subName := sub
if subStructMap != nil {
if val, ok := subStructMap[sub]; ok {
subName = val
} else {
//subName = fmt.Sprintf("%v_sub%v", structName, len(subStructMap)+1)
subName = FmtFieldName(key) // use the field name word
subStructMap[sub] = subName
}
}
valueType = "[]" + subName
}
}
case map[interface{}]interface{}:
sub := generateTypes(convertKeysToStrings(value), structName, tags, depth+1, subStructMap, convertFloats) + "}"
subName := sub
if subStructMap != nil {
if val, ok := subStructMap[sub]; ok {
subName = val
} else {
//subName = fmt.Sprintf("%v_sub%v", structName, len(subStructMap)+1)
subName = FmtFieldName(key) // use the field name word
subStructMap[sub] = subName
}
}
valueType = subName
case map[string]interface{}:
sub := generateTypes(value, structName, tags, depth+1, subStructMap, convertFloats) + "}"
subName := sub
if subStructMap != nil {
if val, ok := subStructMap[sub]; ok {
subName = val
} else {
//subName = fmt.Sprintf("%v_sub%v", structName, len(subStructMap)+1)
subName = FmtFieldName(key) // use the field name word
subStructMap[sub] = subName
}
}
valueType = subName
}
fieldName := FmtFieldName(key)
tagList := make([]string, 0)
for _, t := range tags {
tagList = append(tagList, fmt.Sprintf("%s:\"%s\"", t, key))
}
structure += fmt.Sprintf("\n%s %s `%s`",
fieldName,
valueType,
strings.Join(tagList, " "))
}
return structure
}
// FmtFieldName formats a string as a struct key
//
// Example:
//
// FmtFieldName("foo_id")
//
// Output: FooID
func FmtFieldName(s string) string {
runes := []rune(s)
for len(runes) > 0 && !unicode.IsLetter(runes[0]) && !unicode.IsDigit(runes[0]) {
runes = runes[1:]
}
if len(runes) == 0 {
return "_"
}
s = stringifyFirstChar(string(runes))
name := lintFieldName(s)
runes = []rune(name)
for i, c := range runes {
ok := unicode.IsLetter(c) || unicode.IsDigit(c)
if i == 0 {
ok = unicode.IsLetter(c)
}
if !ok {
runes[i] = '_'
}
}
s = string(runes)
s = strings.Trim(s, "_")
if len(s) == 0 {
return "_"
}
return s
}
// nolint
func lintFieldName(name string) string {
// Fast path for simple cases: "_" and all lowercase.
if name == "_" {
return name
}
allLower := true
for _, r := range name {
if !unicode.IsLower(r) {
allLower = false
break
}
}
if allLower {
runes := []rune(name)
if u := strings.ToUpper(name); commonInitialisms[u] {
copy(runes[0:], []rune(u))
} else {
runes[0] = unicode.ToUpper(runes[0])
}
return string(runes)
}
allUpperWithUnderscore := true
for _, r := range name {
if !unicode.IsUpper(r) && r != '_' {
allUpperWithUnderscore = false
break
}
}
if allUpperWithUnderscore {
name = strings.ToLower(name)
}
// Split camelCase at any lower->upper transition, and split on underscores.
// Check each word for common initialisms.
runes := []rune(name)
w, i := 0, 0 // index of start of word, scan
for i+1 <= len(runes) {
eow := false // whether we hit the end of a word
if i+1 == len(runes) {
eow = true
} else if runes[i+1] == '_' {
// underscore; shift the remainder forward over any run of underscores
eow = true
n := 1
for i+n+1 < len(runes) && runes[i+n+1] == '_' {
n++
}
// Leave at most one underscore if the underscore is between two digits
if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
n--
}
copy(runes[i+1:], runes[i+n+1:])
runes = runes[:len(runes)-n]
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
// lower->non-lower
eow = true
}
i++
if !eow {
continue
}
// [w,i) is a word.
word := string(runes[w:i])
if u := strings.ToUpper(word); commonInitialisms[u] {
// All the common initialisms are ASCII,
// so we can replace the bytes exactly.
copy(runes[w:], []rune(u))
} else if strings.ToLower(word) == word {
// already all lowercase, and not the first word, so uppercase the first character.
runes[w] = unicode.ToUpper(runes[w])
}
w = i
}
return string(runes)
}
// generate an appropriate struct type entry
func typeForValue(value interface{}, structName string, tags []string, subStructMap map[string]string, convertFloats bool) string {
//Check if this is an array
if objects, ok := value.([]interface{}); ok {
types := make(map[reflect.Type]bool, 0)
for _, o := range objects {
types[reflect.TypeOf(o)] = true
}
if len(types) == 1 {
return "[]" + typeForValue(mergeElements(objects).([]interface{})[0], structName, tags, subStructMap, convertFloats)
}
return "[]interface{}"
} else if object, ok := value.(map[interface{}]interface{}); ok {
return generateTypes(convertKeysToStrings(object), structName, tags, 0, subStructMap, convertFloats) + "}"
} else if object, ok := value.(map[string]interface{}); ok {
return generateTypes(object, structName, tags, 0, subStructMap, convertFloats) + "}"
} else if reflect.TypeOf(value) == nil {
return "interface{}"
}
v := reflect.TypeOf(value).Name()
if v == "float64" && convertFloats {
v = disambiguateFloatInt(value)
}
return v
}
// All numbers will initially be read as float64
// If the number appears to be an integer value, use int instead
func disambiguateFloatInt(value interface{}) string {
const epsilon = .0001
vfloat := value.(float64)
if !ForceFloats && math.Abs(vfloat-math.Floor(vfloat+epsilon)) < epsilon {
var tmp int64
return reflect.TypeOf(tmp).Name()
}
return reflect.TypeOf(value).Name()
}
// convert first character ints to strings
func stringifyFirstChar(str string) string {
first := str[:1]
i, err := strconv.ParseInt(first, 10, 8)
if err != nil {
return str
}
return intToWordMap[i] + "_" + str[1:]
}
func mergeElements(i interface{}) interface{} {
switch i := i.(type) {
default:
return i
case []interface{}:
l := len(i)
if l == 0 {
return i
}
for j := 1; j < l; j++ {
i[0] = mergeObjects(i[0], i[j])
}
return i[0:1]
}
}
func mergeObjects(o1, o2 interface{}) interface{} {
if o1 == nil {
return o2
}
if o2 == nil {
return o1
}
if reflect.TypeOf(o1) != reflect.TypeOf(o2) {
return nil
}
switch i := o1.(type) {
default:
return o1
case []interface{}:
if i2, ok := o2.([]interface{}); ok {
i3 := append(i, i2...)
return mergeElements(i3)
}
return mergeElements(i)
case map[string]interface{}:
if i2, ok := o2.(map[string]interface{}); ok {
for k, v := range i2 {
if v2, ok := i[k]; ok {
i[k] = mergeObjects(v2, v)
} else {
i[k] = v
}
}
}
return i
case map[interface{}]interface{}:
if i2, ok := o2.(map[interface{}]interface{}); ok {
for k, v := range i2 {
if v2, ok := i[k]; ok {
i[k] = mergeObjects(v2, v)
} else {
i[k] = v
}
}
}
return i
}
}
package jy2struct
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseError(t *testing.T) {
testData := `foo:bar`
r := strings.NewReader(testData)
_, err := ParseJSON(r)
assert.Error(t, err)
testData = ` foo: bar`
r = strings.NewReader(testData)
_, err = ParseYaml(r)
assert.Error(t, err)
_, err = jyParse(r, ParseYaml, "", "", nil, false, false)
assert.Error(t, err)
v := FmtFieldName("")
v = lintFieldName(v)
assert.Equal(t, "_", v)
v = stringifyFirstChar("2foo")
assert.Equal(t, "two_foo", v)
}
func Test_convertKeysToStrings(t *testing.T) {
testData := map[interface{}]interface{}{"foo": "bar"}
v := convertKeysToStrings(testData)
assert.NotNil(t, v)
}
func Test_mergeElements(t *testing.T) {
testData := "foo"
v := mergeElements(testData)
assert.Equal(t, testData, v)
testData2 := []interface{}{}
v = mergeElements(testData2)
assert.Empty(t, v)
testData2 = []interface{}{"foo", "bar"}
v = mergeElements(testData2)
assert.Equal(t, testData2[0], v.([]interface{})[0])
}
func Test_mergeObjects(t *testing.T) {
var (
o1 = []interface{}{"foo", "bar"}
o2 = map[string]interface{}{"foo": "bar"}
o3 = map[interface{}]interface{}{"foo": "bar"}
)
v := mergeObjects(nil, o2)
assert.Equal(t, o2, v)
v = mergeObjects(o1, nil)
assert.Equal(t, o1, v)
v = mergeObjects(o1, o2)
assert.Nil(t, v)
v = mergeObjects("foo", "bar")
assert.Equal(t, "foo", v)
v = mergeObjects(o1, o1)
t.Log(v)
v = mergeObjects(o2, o2)
t.Log(v)
v = mergeObjects(o3, o3)
t.Log(v)
}
[
{
"name": "foo",
"age": 10,
"email": "foo@bar.com",
"companies": [
{
"name":"foo",
"address":"foo",
"position": "foo"
},
{
"name":"foo2",
"address":"foo2",
"position": "foo2"
}
]
}
]
\ No newline at end of file
serverName: "demo"
serverPort: 8080
runMode: "dev"
jwt:
signingKey: "abcd"
expire: 86400
email:
sender: "foo@bar.com"
password: "1234"
log:
level: "debug"
format: "console"
isSave: true
package kafka
import (
"fmt"
"github.com/IBM/sarama"
)
// ClientManager client manager
type ClientManager struct {
client sarama.Client
offsetManager sarama.OffsetManager
}
// Backlog info
type Backlog struct {
Partition int32 `json:"partition"` // partition id
Backlog int64 `json:"backlog"` // data backlog
NextConsumeOffset int64 `json:"nextOffset"` // offset for next consumption
}
// InitClientManager init client manager
func InitClientManager(addrs []string, groupID string) (*ClientManager, error) {
config := sarama.NewConfig()
client, err := sarama.NewClient(addrs, config)
if err != nil {
return nil, err
}
offsetManager, err := sarama.NewOffsetManagerFromClient(groupID, client)
if err != nil {
return nil, err
}
return &ClientManager{
client: client,
offsetManager: offsetManager,
}, nil
}
// GetBacklog get topic backlog
func (m *ClientManager) GetBacklog(topic string) (int64, []*Backlog, error) {
if m == nil || m.client == nil {
return 0, nil, fmt.Errorf("client manager is nil")
}
var (
total int64
partitionBacklogs []*Backlog
)
partitions, err := m.client.Partitions(topic)
if err != nil {
return 0, nil, err
}
for _, partition := range partitions {
// get offset from kafka
offset, err := m.client.GetOffset(topic, partition, -1)
if err != nil {
return 0, nil, err
}
// create topic/partition manager
pom, err := m.offsetManager.ManagePartition(topic, partition)
if err != nil {
return 0, nil, err
}
var backlog int64
// call sarama The NextOffset method of PartitionOffsetManager. Return the offset for the next consumption
// if the consumer group has not consumed the data for this section, the return value will be -1
n, str := pom.NextOffset()
if str != "" {
return 0, nil, fmt.Errorf("partition %d, %s", partition, str)
}
if n == -1 {
backlog = offset
} else {
backlog = offset - n
}
total += backlog
partitionBacklogs = append(partitionBacklogs, &Backlog{
Partition: partition,
Backlog: backlog,
NextConsumeOffset: n,
})
}
return total, partitionBacklogs, nil
}
// Close topic backlog
func (m *ClientManager) Close() error {
if m != nil && m.client != nil {
return m.client.Close()
}
return nil
}
package kafka
import (
"testing"
"github.com/IBM/sarama"
)
func TestInitClientManager(t *testing.T) {
m, err := InitClientManager(addrs, groupID)
if err != nil {
t.Log(err)
return
}
defer m.Close()
}
func testConfig() *sarama.Config {
config := sarama.NewConfig()
config.Consumer.Retry.Backoff = 0
config.Producer.Retry.Backoff = 0
config.Version = sarama.MinVersion
config.Metadata.Retry.Max = 0
return config
}
func TestClientManager_GetBacklog(t *testing.T) {
seedBroker := sarama.NewMockBroker(t, 1)
leader := sarama.NewMockBroker(t, 2)
metadata := new(sarama.MetadataResponse)
metadata.AddTopicPartition("foo", 0, leader.BrokerID(), nil, nil, nil, sarama.ErrNoError)
metadata.AddTopicPartition("foo", 1, leader.BrokerID(), nil, nil, nil, sarama.ErrNoError)
metadata.AddBroker(leader.Addr(), leader.BrokerID())
seedBroker.Returns(metadata)
client, err := sarama.NewClient([]string{seedBroker.Addr()}, testConfig())
if err != nil {
t.Fatal(err)
}
offsetResponse := new(sarama.OffsetResponse)
offsetResponse.AddTopicPartition("foo", 0, 123)
leader.Returns(offsetResponse)
leader.Returns(&sarama.ConsumerMetadataResponse{
Coordinator: sarama.NewBroker(leader.Addr()),
})
offsetManager, err := sarama.NewOffsetManagerFromClient("group", client)
if err != nil {
t.Error(err)
return
}
fetchResponse := new(sarama.OffsetFetchResponse)
fetchResponse.AddBlock("foo", 0, &sarama.OffsetFetchResponseBlock{
Err: sarama.ErrNoError,
Offset: 123,
Metadata: "original_meta",
})
leader.Returns(fetchResponse)
m := ClientManager{
client: client,
offsetManager: offsetManager,
}
defer m.Close()
total, backlogs, err := m.GetBacklog("foo")
if err != nil {
t.Log(err)
return
}
t.Log(total, backlogs)
}
package kafka
import (
"context"
"github.com/IBM/sarama"
"go.uber.org/zap"
)
// ---------------------------------- consume group---------------------------------------
// ConsumerGroup consume group
type ConsumerGroup struct {
Group sarama.ConsumerGroup
groupID string
zapLogger *zap.Logger
autoCommitEnable bool
}
// InitConsumerGroup init consumer group
func InitConsumerGroup(addrs []string, groupID string, opts ...ConsumerOption) (*ConsumerGroup, error) {
o := defaultConsumerOptions()
o.apply(opts...)
var config *sarama.Config
if o.config != nil {
config = o.config
} else {
config = sarama.NewConfig()
config.Version = o.version
config.Consumer.Group.Rebalance.GroupStrategies = o.groupStrategies
config.Consumer.Offsets.Initial = o.offsetsInitial
config.Consumer.Offsets.AutoCommit.Enable = o.offsetsAutoCommitEnable
config.Consumer.Offsets.AutoCommit.Interval = o.offsetsAutoCommitInterval
config.ClientID = o.clientID
if o.tlsConfig != nil {
config.Net.TLS.Config = o.tlsConfig
config.Net.TLS.Enable = true
}
}
consumer, err := sarama.NewConsumerGroup(addrs, groupID, config)
if err != nil {
return nil, err
}
return &ConsumerGroup{
Group: consumer,
groupID: groupID,
zapLogger: o.zapLogger,
autoCommitEnable: config.Consumer.Offsets.AutoCommit.Enable,
}, nil
}
// Consume consume messages
func (c *ConsumerGroup) Consume(ctx context.Context, topics []string, handleMessageFn HandleMessageFn) error {
handler := &defaultConsumerHandler{
ctx: ctx,
handleMessageFn: handleMessageFn,
zapLogger: c.zapLogger,
autoCommitEnable: c.autoCommitEnable,
}
err := c.Group.Consume(ctx, topics, handler)
if err != nil {
c.zapLogger.Error("failed to consume messages", zap.String("group_id", c.groupID), zap.Strings("topics", topics), zap.Error(err))
return err
}
return nil
}
// ConsumeCustom consume messages for custom handler, you need to implement the sarama.ConsumerGroupHandler interface
func (c *ConsumerGroup) ConsumeCustom(ctx context.Context, topics []string, handler sarama.ConsumerGroupHandler) error {
err := c.Group.Consume(ctx, topics, handler)
if err != nil {
c.zapLogger.Error("failed to consume messages", zap.String("group_id", c.groupID), zap.Strings("topics", topics), zap.Error(err))
return err
}
return nil
}
func (c *ConsumerGroup) Close() error {
if c == nil || c.Group == nil {
return c.Group.Close()
}
return nil
}
type defaultConsumerHandler struct {
ctx context.Context
handleMessageFn HandleMessageFn
zapLogger *zap.Logger
autoCommitEnable bool
}
// Setup is run at the beginning of a new session, before ConsumeClaim
func (h *defaultConsumerHandler) Setup(sess sarama.ConsumerGroupSession) error {
h.zapLogger.Info("consumer group session [setup]", zap.Any("claims", sess.Claims()))
return nil
}
// Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited
func (h *defaultConsumerHandler) Cleanup(sess sarama.ConsumerGroupSession) error {
h.zapLogger.Info("consumer group session [cleanup]", zap.Any("claims", sess.Claims()))
return nil
}
// ConsumeClaim consumes messages
func (h *defaultConsumerHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
defer func() {
if e := recover(); e != nil {
h.zapLogger.Error("panic occurred while consuming messages", zap.Any("error", e))
_ = h.ConsumeClaim(sess, claim)
}
}()
for {
select {
case <-h.ctx.Done():
return nil
case msg, ok := <-claim.Messages():
if !ok {
return nil
}
err := h.handleMessageFn(msg)
if err != nil {
h.zapLogger.Error("failed to handle message", zap.Error(err))
continue
}
sess.MarkMessage(msg, "")
if !h.autoCommitEnable {
sess.Commit()
}
}
}
}
// ---------------------------------- consume partition------------------------------------
// Consumer consume partition
type Consumer struct {
C sarama.Consumer
zapLogger *zap.Logger
}
// InitConsumer init consumer
func InitConsumer(addrs []string, opts ...ConsumerOption) (*Consumer, error) {
o := defaultConsumerOptions()
o.apply(opts...)
var config *sarama.Config
if o.config != nil {
config = o.config
} else {
config = sarama.NewConfig()
config.Version = o.version
config.Consumer.Return.Errors = true
config.ClientID = o.clientID
if o.tlsConfig != nil {
config.Net.TLS.Config = o.tlsConfig
config.Net.TLS.Enable = true
}
}
consumer, err := sarama.NewConsumer(addrs, config)
if err != nil {
return nil, err
}
return &Consumer{
C: consumer,
zapLogger: o.zapLogger,
}, nil
}
// ConsumePartition consumer one partition, blocking
func (c *Consumer) ConsumePartition(ctx context.Context, topic string, partition int32, offset int64, handleFn HandleMessageFn) {
defer func() {
if e := recover(); e != nil {
c.zapLogger.Error("panic occurred while consuming messages", zap.Any("error", e))
c.ConsumePartition(ctx, topic, partition, offset, handleFn)
}
}()
pc, err := c.C.ConsumePartition(topic, partition, offset)
if err != nil {
c.zapLogger.Error("failed to create partition consumer", zap.Error(err), zap.String("topic", topic), zap.Int32("partition", partition))
return
}
c.zapLogger.Info("start consuming partition", zap.String("topic", topic), zap.Int32("partition", partition), zap.Int64("offset", offset))
for {
select {
case msg := <-pc.Messages():
err := handleFn(msg)
if err != nil {
c.zapLogger.Warn("failed to handle message", zap.Error(err), zap.String("topic", topic), zap.Int32("partition", partition), zap.Int64("offset", msg.Offset))
}
case err := <-pc.Errors():
c.zapLogger.Error("partition consumer error", zap.Error(err))
case <-ctx.Done():
return
}
}
}
// ConsumeAllPartition consumer all partitions, no blocking
func (c *Consumer) ConsumeAllPartition(ctx context.Context, topic string, offset int64, handleFn HandleMessageFn) {
partitionList, err := c.C.Partitions(topic)
if err != nil {
c.zapLogger.Error("failed to get partition", zap.Error(err))
return
}
for _, partition := range partitionList {
go func(partition int32, offset int64) {
c.ConsumePartition(ctx, topic, partition, offset, handleFn)
}(partition, offset)
}
}
// Close the consumer
func (c *Consumer) Close() error {
if c == nil || c.C == nil {
return c.C.Close()
}
return nil
}
package kafka
import (
"crypto/tls"
"fmt"
"time"
"github.com/IBM/sarama"
"go.uber.org/zap"
)
// HandleMessageFn is a function that handles a message from a partition consumer
type HandleMessageFn func(msg *sarama.ConsumerMessage) error
// ConsumerOption set options.
type ConsumerOption func(*consumerOptions)
type consumerOptions struct {
version sarama.KafkaVersion // default V2_1_0_0
clientID string // default "sarama"
tlsConfig *tls.Config // default nil
// consumer group options
groupStrategies []sarama.BalanceStrategy // default NewBalanceStrategyRange
offsetsInitial int64 // default OffsetOldest
offsetsAutoCommitEnable bool // default true
offsetsAutoCommitInterval time.Duration // default 1s, when offsetsAutoCommitEnable is true
// custom config, if not nil, it will override the default config, the above parameters are invalid
config *sarama.Config // default nil
zapLogger *zap.Logger // default NewProduction
}
func (o *consumerOptions) apply(opts ...ConsumerOption) {
for _, opt := range opts {
opt(o)
}
}
func defaultConsumerOptions() *consumerOptions {
zapLogger, _ := zap.NewProduction()
return &consumerOptions{
version: sarama.V2_1_0_0,
groupStrategies: []sarama.BalanceStrategy{sarama.NewBalanceStrategyRange()},
offsetsInitial: sarama.OffsetOldest,
offsetsAutoCommitEnable: true,
offsetsAutoCommitInterval: time.Second,
clientID: "sarama",
zapLogger: zapLogger,
}
}
// ConsumerWithVersion set kafka version.
func ConsumerWithVersion(version sarama.KafkaVersion) ConsumerOption {
return func(o *consumerOptions) {
o.version = version
}
}
// ConsumerWithGroupStrategies set groupStrategies.
func ConsumerWithGroupStrategies(groupStrategies ...sarama.BalanceStrategy) ConsumerOption {
return func(o *consumerOptions) {
if len(groupStrategies) > 0 {
o.groupStrategies = groupStrategies
}
}
}
// ConsumerWithOffsetsInitial set offsetsInitial.
func ConsumerWithOffsetsInitial(offsetsInitial int64) ConsumerOption {
return func(o *consumerOptions) {
o.offsetsInitial = offsetsInitial
}
}
// ConsumerWithOffsetsAutoCommitEnable set offsetsAutoCommitEnable.
func ConsumerWithOffsetsAutoCommitEnable(offsetsAutoCommitEnable bool) ConsumerOption {
return func(o *consumerOptions) {
o.offsetsAutoCommitEnable = offsetsAutoCommitEnable
}
}
// ConsumerWithOffsetsAutoCommitInterval set offsetsAutoCommitInterval.
func ConsumerWithOffsetsAutoCommitInterval(offsetsAutoCommitInterval time.Duration) ConsumerOption {
return func(o *consumerOptions) {
o.offsetsAutoCommitInterval = offsetsAutoCommitInterval
}
}
// ConsumerWithClientID set clientID.
func ConsumerWithClientID(clientID string) ConsumerOption {
return func(o *consumerOptions) {
o.clientID = clientID
}
}
// ConsumerWithTLS set tlsConfig, if isSkipVerify is true, crypto/tls accepts any certificate presented by
// the server and any host name in that certificate.
func ConsumerWithTLS(certFile, keyFile, caFile string, isSkipVerify bool) ConsumerOption {
return func(o *consumerOptions) {
var err error
o.tlsConfig, err = getTLSConfig(certFile, keyFile, caFile, isSkipVerify)
if err != nil {
fmt.Println("ConsumerWithTLS error:", err)
}
}
}
// ConsumerWithZapLogger set zapLogger.
func ConsumerWithZapLogger(zapLogger *zap.Logger) ConsumerOption {
return func(o *consumerOptions) {
if zapLogger != nil {
o.zapLogger = zapLogger
}
}
}
// ConsumerWithConfig set custom config.
func ConsumerWithConfig(config *sarama.Config) ConsumerOption {
return func(o *consumerOptions) {
o.config = config
}
}
package kafka
import (
"context"
"fmt"
"testing"
"time"
"github.com/IBM/sarama"
"go.uber.org/zap"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/gtls/certfile"
)
var (
groupID = "my-group"
waitTime = time.Second * 10
handleMsgFn = func(msg *sarama.ConsumerMessage) error {
fmt.Printf("received msg: topic=%s, partition=%d, offset=%d, key=%s, val=%s\n",
msg.Topic, msg.Partition, msg.Offset, msg.Key, msg.Value)
return nil
}
)
type myConsumerGroupHandler struct {
autoCommitEnable bool
}
func (h *myConsumerGroupHandler) Setup(session sarama.ConsumerGroupSession) error {
return nil
}
func (h *myConsumerGroupHandler) Cleanup(session sarama.ConsumerGroupSession) error {
return nil
}
func (h *myConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
fmt.Printf("received msg: topic=%s, partition=%d, offset=%d, key=%s, val=%s\n",
msg.Topic, msg.Partition, msg.Offset, msg.Key, msg.Value)
session.MarkMessage(msg, "")
if !h.autoCommitEnable {
session.Commit()
}
}
return nil
}
func TestInitConsumerGroup(t *testing.T) {
// Test InitConsumerGroup default options
cg, err := InitConsumerGroup(addrs, groupID)
if err != nil {
t.Log(err)
}
// Test InitConsumerGroup with options
cg, err = InitConsumerGroup(addrs, groupID,
ConsumerWithVersion(sarama.V3_6_0_0),
ConsumerWithClientID("my-client-id"),
ConsumerWithGroupStrategies(sarama.NewBalanceStrategySticky()),
ConsumerWithOffsetsInitial(sarama.OffsetOldest),
ConsumerWithOffsetsAutoCommitEnable(true),
ConsumerWithOffsetsAutoCommitInterval(time.Second),
ConsumerWithTLS(certfile.Path("two-way/server/server.pem"), certfile.Path("two-way/server/server.key"), certfile.Path("two-way/ca.pem"), true),
ConsumerWithZapLogger(zap.NewNop()),
)
if err != nil {
t.Log(err)
}
// Test InitConsumerGroup custom options
config := sarama.NewConfig()
config.Producer.Return.Successes = true
cg, err = InitConsumerGroup(addrs, groupID, ConsumerWithConfig(config))
if err != nil {
t.Log(err)
return
}
time.Sleep(time.Second)
_ = cg.Close()
}
func TestConsumerGroup_Consume(t *testing.T) {
cg, err := InitConsumerGroup(addrs, groupID,
ConsumerWithVersion(sarama.V3_6_0_0),
ConsumerWithOffsetsInitial(sarama.OffsetOldest),
ConsumerWithOffsetsAutoCommitEnable(true),
ConsumerWithOffsetsAutoCommitInterval(time.Second),
)
if err != nil {
t.Log(err)
return
}
defer cg.Close()
go cg.Consume(context.Background(), []string{testTopic}, handleMsgFn)
<-time.After(waitTime)
}
func TestConsumerGroup_ConsumeCustom(t *testing.T) {
cg, err := InitConsumerGroup(addrs, groupID,
ConsumerWithVersion(sarama.V3_6_0_0),
ConsumerWithOffsetsAutoCommitEnable(false),
)
if err != nil {
t.Log(err)
return
}
defer cg.Close()
cgh := &myConsumerGroupHandler{autoCommitEnable: cg.autoCommitEnable}
go cg.ConsumeCustom(context.Background(), []string{testTopic}, cgh)
<-time.After(waitTime)
}
func TestInitConsumer(t *testing.T) {
// Test InitConsumer default options
c, err := InitConsumer(addrs)
if err != nil {
t.Log(err)
}
// Test InitConsumer with options
c, err = InitConsumer(addrs,
ConsumerWithVersion(sarama.V3_6_0_0),
ConsumerWithClientID("my-client-id"),
ConsumerWithTLS(certfile.Path("two-way/server/server.pem"), certfile.Path("two-way/server/server.key"), certfile.Path("two-way/ca.pem"), true),
ConsumerWithZapLogger(zap.NewNop()),
)
if err != nil {
t.Log(err)
}
// Test InitConsumer custom options
config := sarama.NewConfig()
config.Producer.Return.Successes = true
c, err = InitConsumer(addrs, ConsumerWithConfig(config))
if err != nil {
t.Log(err)
return
}
time.Sleep(time.Second)
_ = c.Close()
}
func TestConsumer_ConsumePartition(t *testing.T) {
c, err := InitConsumer(addrs,
ConsumerWithVersion(sarama.V3_6_0_0),
ConsumerWithClientID("my-client-id"),
)
if err != nil {
t.Log(err)
return
}
defer c.Close()
go c.ConsumePartition(context.Background(), testTopic, 0, sarama.OffsetNewest, handleMsgFn)
<-time.After(waitTime)
}
func TestConsumer_ConsumeAllPartition(t *testing.T) {
c, err := InitConsumer(addrs,
ConsumerWithVersion(sarama.V3_6_0_0),
ConsumerWithClientID("my-client-id"),
)
if err != nil {
t.Log(err)
return
}
defer c.Close()
c.ConsumeAllPartition(context.Background(), testTopic, sarama.OffsetNewest, handleMsgFn)
<-time.After(waitTime)
}
func TestConsumerGroup(t *testing.T) {
var (
myTopic = "my-topic"
myGroup = "my_group"
)
broker0 := sarama.NewMockBroker(t, 0)
defer broker0.Close()
mockData := map[string]sarama.MockResponse{
"MetadataRequest": sarama.NewMockMetadataResponse(t).
SetBroker(broker0.Addr(), broker0.BrokerID()).
SetLeader(myTopic, 0, broker0.BrokerID()),
"OffsetRequest": sarama.NewMockOffsetResponse(t).
SetOffset(myTopic, 0, sarama.OffsetOldest, 0).
SetOffset(myTopic, 0, sarama.OffsetNewest, 1),
"FindCoordinatorRequest": sarama.NewMockFindCoordinatorResponse(t).
SetCoordinator(sarama.CoordinatorGroup, myGroup, broker0),
"HeartbeatRequest": sarama.NewMockHeartbeatResponse(t),
"JoinGroupRequest": sarama.NewMockSequence(
sarama.NewMockJoinGroupResponse(t).SetError(sarama.ErrOffsetsLoadInProgress),
sarama.NewMockJoinGroupResponse(t).SetGroupProtocol(sarama.RangeBalanceStrategyName),
),
"SyncGroupRequest": sarama.NewMockSequence(
sarama.NewMockSyncGroupResponse(t).SetError(sarama.ErrOffsetsLoadInProgress),
sarama.NewMockSyncGroupResponse(t).SetMemberAssignment(
&sarama.ConsumerGroupMemberAssignment{
Version: 0,
Topics: map[string][]int32{
myTopic: {0},
},
}),
),
"OffsetFetchRequest": sarama.NewMockOffsetFetchResponse(t).SetOffset(
myGroup, myTopic, 0, 0, "", sarama.ErrNoError,
).SetError(sarama.ErrNoError),
"FetchRequest": sarama.NewMockSequence(
sarama.NewMockFetchResponse(t, 1).
SetMessage(myTopic, 0, 0, sarama.StringEncoder("foo")).
SetMessage(myTopic, 0, 1, sarama.StringEncoder("bar")),
sarama.NewMockFetchResponse(t, 1),
),
}
broker0.SetHandlerByMap(mockData)
config := sarama.NewConfig()
config.ClientID = t.Name()
config.Version = sarama.V2_0_0_0
config.Consumer.Return.Errors = true
config.Consumer.Group.Rebalance.Retry.Max = 2
config.Consumer.Group.Rebalance.Retry.Backoff = 0
config.Consumer.Offsets.AutoCommit.Enable = false
group, err := sarama.NewConsumerGroup([]string{broker0.Addr()}, myGroup, config)
if err != nil {
t.Fatal(err)
}
topics := []string{myTopic}
g := &ConsumerGroup{
Group: group,
groupID: myGroup,
zapLogger: zap.NewExample(),
autoCommitEnable: false,
}
defer g.Close()
ctx, cancel := context.WithCancel(context.Background())
go g.Consume(ctx, topics, handleMsgFn)
<-time.After(time.Second)
broker0.SetHandlerByMap(mockData)
group, err = sarama.NewConsumerGroup([]string{broker0.Addr()}, myGroup, config)
if err != nil {
t.Fatal(err)
}
g.Group = group
go g.ConsumeCustom(ctx, topics, &defaultConsumerHandler{
ctx: ctx,
handleMessageFn: handleMsgFn,
zapLogger: g.zapLogger,
autoCommitEnable: g.autoCommitEnable,
})
<-time.After(time.Second)
cancel()
}
func TestConsumerPartition(t *testing.T) {
myTopic := "my-topic"
testMsg := sarama.StringEncoder("Foo")
broker0 := sarama.NewMockBroker(t, 0)
manualOffset := int64(1234)
offsetNewest := int64(2345)
offsetNewestAfterFetchRequest := int64(3456)
mockFetchResponse := sarama.NewMockFetchResponse(t, 1)
mockFetchResponse.SetMessage(myTopic, 0, manualOffset-1, testMsg)
for i := int64(0); i < 10; i++ {
mockFetchResponse.SetMessage(myTopic, 0, i+manualOffset, testMsg)
}
mockFetchResponse.SetHighWaterMark(myTopic, 0, offsetNewestAfterFetchRequest)
mockData := map[string]sarama.MockResponse{
"MetadataRequest": sarama.NewMockMetadataResponse(t).
SetBroker(broker0.Addr(), broker0.BrokerID()).
SetLeader(myTopic, 0, broker0.BrokerID()),
"OffsetRequest": sarama.NewMockOffsetResponse(t).
SetOffset(myTopic, 0, sarama.OffsetOldest, 0).
SetOffset(myTopic, 0, sarama.OffsetNewest, offsetNewest),
"FetchRequest": mockFetchResponse,
}
broker0.SetHandlerByMap(mockData)
master, err := sarama.NewConsumer([]string{broker0.Addr()}, sarama.NewConfig())
if err != nil {
t.Fatal(err)
}
c := &Consumer{
C: master,
zapLogger: zap.NewExample(),
}
defer c.Close()
ctx, cancel := context.WithCancel(context.Background())
go c.ConsumePartition(ctx, myTopic, 0, manualOffset, handleMsgFn)
<-time.After(time.Second)
broker0.SetHandlerByMap(mockData)
master, err = sarama.NewConsumer([]string{broker0.Addr()}, sarama.NewConfig())
if err != nil {
t.Fatal(err)
}
c.C = master
go c.ConsumeAllPartition(ctx, myTopic, offsetNewest, handleMsgFn)
<-time.After(time.Second)
cancel()
}
// Package kafka is a kafka client package.
package kafka
import (
"encoding/json"
"fmt"
"github.com/IBM/sarama"
"go.uber.org/zap"
)
// ProducerMessage is sarama ProducerMessage
type ProducerMessage = sarama.ProducerMessage
// ---------------------------------- sync producer ---------------------------------------
// SyncProducer is a sync producer.
type SyncProducer struct {
Producer sarama.SyncProducer
}
// InitSyncProducer init sync producer.
func InitSyncProducer(addrs []string, opts ...SyncProducerOption) (*SyncProducer, error) {
o := defaultSyncProducerOptions()
o.apply(opts...)
var config *sarama.Config
if o.config != nil {
config = o.config
} else {
config = sarama.NewConfig()
config.Version = o.version
config.Producer.RequiredAcks = o.requiredAcks
config.Producer.Partitioner = o.partitioner
config.Producer.Return.Successes = o.returnSuccesses
config.ClientID = o.clientID
if o.tlsConfig != nil {
config.Net.TLS.Config = o.tlsConfig
config.Net.TLS.Enable = true
}
}
producer, err := sarama.NewSyncProducer(addrs, config)
if err != nil {
return nil, err
}
return &SyncProducer{Producer: producer}, nil
}
// SendMessage sends a message to a topic.
func (p *SyncProducer) SendMessage(msg *sarama.ProducerMessage) (int32, int64, error) {
return p.Producer.SendMessage(msg)
}
// SendData sends a message to a topic with multiple types of data.
func (p *SyncProducer) SendData(topic string, data interface{}) (int32, int64, error) {
var msg *sarama.ProducerMessage
switch val := data.(type) {
case *sarama.ProducerMessage:
msg = val
case []byte:
msg = &sarama.ProducerMessage{Topic: topic, Value: sarama.ByteEncoder(val)}
case string:
msg = &sarama.ProducerMessage{Topic: topic, Value: sarama.StringEncoder(val)}
case *Message:
msg = &sarama.ProducerMessage{Topic: val.Topic, Value: sarama.ByteEncoder(val.Data), Key: sarama.ByteEncoder(val.Key)}
default:
buf, err := json.Marshal(data)
if err != nil {
return 0, 0, err
}
msg = &sarama.ProducerMessage{Topic: topic, Value: sarama.ByteEncoder(buf)}
}
return p.Producer.SendMessage(msg)
}
// Close closes the producer.
func (p *SyncProducer) Close() error {
if p.Producer != nil {
return p.Producer.Close()
}
return nil
}
// Message is a message to be sent to a topic.
type Message struct {
Topic string `json:"topic"`
Data []byte `json:"data"`
Key []byte `json:"key"`
}
// ---------------------------------- async producer ---------------------------------------
// AsyncProducer is async producer.
type AsyncProducer struct {
Producer sarama.AsyncProducer
zapLogger *zap.Logger
exit chan struct{}
}
// InitAsyncProducer init async producer.
func InitAsyncProducer(addrs []string, opts ...AsyncProducerOption) (*AsyncProducer, error) {
o := defaultAsyncProducerOptions()
o.apply(opts...)
var config *sarama.Config
if o.config != nil {
config = o.config
} else {
config = sarama.NewConfig()
config.Version = o.version
config.Producer.RequiredAcks = o.requiredAcks
config.Producer.Partitioner = o.partitioner
config.Producer.Return.Successes = o.returnSuccesses
config.ClientID = o.clientID
config.Producer.Flush.Messages = o.flushMessages
config.Producer.Flush.Frequency = o.flushFrequency
config.Producer.Flush.Bytes = o.flushBytes
if o.tlsConfig != nil {
config.Net.TLS.Config = o.tlsConfig
config.Net.TLS.Enable = true
}
}
producer, err := sarama.NewAsyncProducer(addrs, config)
if err != nil {
return nil, err
}
p := &AsyncProducer{
Producer: producer,
zapLogger: o.zapLogger,
exit: make(chan struct{}),
}
go p.handleResponse(o.handleFailedFn)
return p, nil
}
// SendMessage sends messages to a topic.
func (p *AsyncProducer) SendMessage(messages ...*sarama.ProducerMessage) error {
for _, msg := range messages {
select {
case p.Producer.Input() <- msg:
case <-p.exit:
return fmt.Errorf("async produce message had exited")
}
}
return nil
}
// SendData sends messages to a topic with multiple types of data.
func (p *AsyncProducer) SendData(topic string, multiData ...interface{}) error {
var messages []*sarama.ProducerMessage
for _, data := range multiData {
var msg *sarama.ProducerMessage
switch val := data.(type) {
case *sarama.ProducerMessage:
msg = val
case []byte:
msg = &sarama.ProducerMessage{Topic: topic, Value: sarama.ByteEncoder(val)}
case string:
msg = &sarama.ProducerMessage{Topic: topic, Value: sarama.StringEncoder(val)}
case *Message:
msg = &sarama.ProducerMessage{Topic: val.Topic, Value: sarama.ByteEncoder(val.Data), Key: sarama.ByteEncoder(val.Key)}
default:
buf, err := json.Marshal(data)
if err != nil {
return err
}
msg = &sarama.ProducerMessage{Topic: topic, Value: sarama.ByteEncoder(buf)}
}
messages = append(messages, msg)
}
return p.SendMessage(messages...)
}
// handleResponse handles the response of async producer, if producer message failed, you can handle it, e.g. add to other queue to handle later.
func (p *AsyncProducer) handleResponse(handleFn AsyncSendFailedHandlerFn) {
defer func() {
if e := recover(); e != nil {
p.zapLogger.Error("panic occurred while processing async message", zap.Any("error", e))
p.handleResponse(handleFn)
}
}()
for {
select {
case pm := <-p.Producer.Successes():
p.zapLogger.Info("async send successfully",
zap.String("topic", pm.Topic),
zap.Int32("partition", pm.Partition),
zap.Int64("offset", pm.Offset))
case err := <-p.Producer.Errors():
p.zapLogger.Error("async send failed", zap.Error(err.Err), zap.Any("msg", err.Msg))
if handleFn != nil {
e := handleFn(err.Msg)
if e != nil {
p.zapLogger.Error("handle failed msg failed", zap.Error(e))
}
}
case <-p.exit:
return
}
}
}
// Close closes the producer.
func (p *AsyncProducer) Close() error {
defer func() { _ = recover() }() // ignore error
close(p.exit)
if p.Producer != nil {
return p.Producer.Close()
}
return nil
}
package kafka
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"time"
"github.com/IBM/sarama"
"go.uber.org/zap"
)
// -------------------------------------- sync producer ------------------------------------
// SyncProducerOption set options.
type SyncProducerOption func(*syncProducerOptions)
type syncProducerOptions struct {
version sarama.KafkaVersion // default V2_1_0_0
requiredAcks sarama.RequiredAcks // default WaitForAll
partitioner sarama.PartitionerConstructor // default NewHashPartitioner
returnSuccesses bool // default true
clientID string // default "sarama"
tlsConfig *tls.Config // default nil
// custom config, if not nil, it will override the default config, the above parameters are invalid
config *sarama.Config // default nil
}
func (o *syncProducerOptions) apply(opts ...SyncProducerOption) {
for _, opt := range opts {
opt(o)
}
}
func defaultSyncProducerOptions() *syncProducerOptions {
return &syncProducerOptions{
version: sarama.V2_1_0_0,
requiredAcks: sarama.WaitForAll,
partitioner: sarama.NewHashPartitioner,
returnSuccesses: true,
clientID: "sarama",
}
}
// SyncProducerWithVersion set kafka version.
func SyncProducerWithVersion(version sarama.KafkaVersion) SyncProducerOption {
return func(o *syncProducerOptions) {
o.version = version
}
}
// SyncProducerWithRequiredAcks set requiredAcks.
func SyncProducerWithRequiredAcks(requiredAcks sarama.RequiredAcks) SyncProducerOption {
return func(o *syncProducerOptions) {
o.requiredAcks = requiredAcks
}
}
// SyncProducerWithPartitioner set partitioner.
func SyncProducerWithPartitioner(partitioner sarama.PartitionerConstructor) SyncProducerOption {
return func(o *syncProducerOptions) {
o.partitioner = partitioner
}
}
// SyncProducerWithReturnSuccesses set returnSuccesses.
func SyncProducerWithReturnSuccesses(returnSuccesses bool) SyncProducerOption {
return func(o *syncProducerOptions) {
o.returnSuccesses = returnSuccesses
}
}
// SyncProducerWithClientID set clientID.
func SyncProducerWithClientID(clientID string) SyncProducerOption {
return func(o *syncProducerOptions) {
o.clientID = clientID
}
}
// SyncProducerWithTLS set tlsConfig, if isSkipVerify is true, crypto/tls accepts any certificate presented by
// the server and any host name in that certificate.
func SyncProducerWithTLS(certFile, keyFile, caFile string, isSkipVerify bool) SyncProducerOption {
return func(o *syncProducerOptions) {
var err error
o.tlsConfig, err = getTLSConfig(certFile, keyFile, caFile, isSkipVerify)
if err != nil {
fmt.Println("SyncProducerWithTLS error:", err)
}
}
}
// SyncProducerWithConfig set custom config.
func SyncProducerWithConfig(config *sarama.Config) SyncProducerOption {
return func(o *syncProducerOptions) {
o.config = config
}
}
// -------------------------------------- async producer -----------------------------------
// AsyncSendFailedHandlerFn is a function that handles failed messages.
type AsyncSendFailedHandlerFn func(msg *sarama.ProducerMessage) error
// AsyncProducerOption set options.
type AsyncProducerOption func(*asyncProducerOptions)
type asyncProducerOptions struct {
version sarama.KafkaVersion // default V2_1_0_0
requiredAcks sarama.RequiredAcks // default WaitForLocal
partitioner sarama.PartitionerConstructor // default NewHashPartitioner
returnSuccesses bool // default true
clientID string // default "sarama"
flushMessages int // default 20
flushFrequency time.Duration // default 2 second
flushBytes int // default 0
tlsConfig *tls.Config
// custom config, if not nil, it will override the default config, the above parameters are invalid
config *sarama.Config // default nil
zapLogger *zap.Logger // default NewProduction
handleFailedFn AsyncSendFailedHandlerFn // default nil
}
func (o *asyncProducerOptions) apply(opts ...AsyncProducerOption) {
for _, opt := range opts {
opt(o)
}
}
func defaultAsyncProducerOptions() *asyncProducerOptions {
zapLogger, _ := zap.NewProduction()
return &asyncProducerOptions{
version: sarama.V2_1_0_0,
requiredAcks: sarama.WaitForLocal,
partitioner: sarama.NewHashPartitioner,
returnSuccesses: true,
clientID: "sarama",
flushMessages: 20,
flushFrequency: 2 * time.Second,
zapLogger: zapLogger,
}
}
// AsyncProducerWithVersion set kafka version.
func AsyncProducerWithVersion(version sarama.KafkaVersion) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.version = version
}
}
// AsyncProducerWithRequiredAcks set requiredAcks.
func AsyncProducerWithRequiredAcks(requiredAcks sarama.RequiredAcks) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.requiredAcks = requiredAcks
}
}
// AsyncProducerWithPartitioner set partitioner.
func AsyncProducerWithPartitioner(partitioner sarama.PartitionerConstructor) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.partitioner = partitioner
}
}
// AsyncProducerWithReturnSuccesses set returnSuccesses.
func AsyncProducerWithReturnSuccesses(returnSuccesses bool) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.returnSuccesses = returnSuccesses
}
}
// AsyncProducerWithClientID set clientID.
func AsyncProducerWithClientID(clientID string) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.clientID = clientID
}
}
// AsyncProducerWithFlushMessages set flushMessages.
func AsyncProducerWithFlushMessages(flushMessages int) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.flushMessages = flushMessages
}
}
// AsyncProducerWithFlushFrequency set flushFrequency.
func AsyncProducerWithFlushFrequency(flushFrequency time.Duration) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.flushFrequency = flushFrequency
}
}
// AsyncProducerWithFlushBytes set flushBytes.
func AsyncProducerWithFlushBytes(flushBytes int) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.flushBytes = flushBytes
}
}
// AsyncProducerWithTLS set tlsConfig, if isSkipVerify is true, crypto/tls accepts any certificate presented by
// the server and any host name in that certificate.
func AsyncProducerWithTLS(certFile, keyFile, caFile string, isSkipVerify bool) AsyncProducerOption {
return func(o *asyncProducerOptions) {
var err error
o.tlsConfig, err = getTLSConfig(certFile, keyFile, caFile, isSkipVerify)
if err != nil {
fmt.Println("AsyncProducerWithTLS error:", err)
}
}
}
// AsyncProducerWithZapLogger set zapLogger.
func AsyncProducerWithZapLogger(zapLogger *zap.Logger) AsyncProducerOption {
return func(o *asyncProducerOptions) {
if zapLogger != nil {
o.zapLogger = zapLogger
}
}
}
// AsyncProducerWithHandleFailed set handleFailedFn.
func AsyncProducerWithHandleFailed(handleFailedFn AsyncSendFailedHandlerFn) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.handleFailedFn = handleFailedFn
}
}
// AsyncProducerWithConfig set custom config.
func AsyncProducerWithConfig(config *sarama.Config) AsyncProducerOption {
return func(o *asyncProducerOptions) {
o.config = config
}
}
func getTLSConfig(certFile, keyFile, caFile string, isSkipVerify bool) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
caCert, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
InsecureSkipVerify: isSkipVerify,
}, nil
}
package kafka
import (
"testing"
"time"
"github.com/IBM/sarama"
"github.com/IBM/sarama/mocks"
"go.uber.org/zap"
"gitlab.wanzhuangkj.com/tush/xpkg/grpc/gtls/certfile"
)
var (
addrs = []string{"localhost:9092"}
//addrs = []string{"192.168.3.37:33001", "192.168.3.37:33002", "192.168.3.37:33003"}
testTopic = "test_topic_1"
testData = []interface{}{
// (1) sarama.ProducerMessage type
&sarama.ProducerMessage{
Topic: testTopic,
Value: sarama.StringEncoder("hello world " + time.Now().String()),
},
// (2) string type
"hello world " + time.Now().String(),
// (3) []byte type
[]byte("hello world " + time.Now().String()),
// (4) struct type, supports json.Marshal
&struct {
Name string `json:"name"`
Age int `json:"age"`
}{
Name: "Alice",
Age: 20,
},
// (5) Message type
&Message{
Topic: testTopic,
Data: []byte("hello world " + time.Now().String()),
Key: []byte("foobar"),
},
}
)
func TestInitSyncProducer(t *testing.T) {
// Test InitSyncProducer default options
p, err := InitSyncProducer(addrs)
if err != nil {
t.Log(err)
}
// Test InitSyncProducer with options
p, err = InitSyncProducer(addrs,
SyncProducerWithVersion(sarama.V3_6_0_0),
SyncProducerWithClientID("my-client-id"),
SyncProducerWithRequiredAcks(sarama.WaitForLocal),
SyncProducerWithPartitioner(sarama.NewRandomPartitioner),
SyncProducerWithReturnSuccesses(true),
SyncProducerWithTLS(certfile.Path("two-way/server/server.pem"), certfile.Path("two-way/server/server.key"), certfile.Path("two-way/ca.pem"), true),
)
if err != nil {
t.Log(err)
}
// Test InitSyncProducer custom options
config := sarama.NewConfig()
config.Producer.Return.Successes = true
p, err = InitSyncProducer(addrs, SyncProducerWithConfig(config))
if err != nil {
t.Log(err)
return
}
time.Sleep(time.Second)
_ = p.Close()
}
func TestSyncProducer_SendMessage(t *testing.T) {
p, err := InitSyncProducer(addrs)
if err != nil {
t.Log(err)
return
}
defer p.Close()
partition, offset, err := p.SendMessage(&sarama.ProducerMessage{
Topic: testTopic,
Value: sarama.StringEncoder("hello world " + time.Now().String()),
})
if err != nil {
t.Error(err)
return
}
t.Log("partition:", partition, "offset:", offset)
}
func TestSyncProducer_SendData(t *testing.T) {
p, err := InitSyncProducer(addrs)
if err != nil {
t.Log(err)
return
}
defer p.Close()
for _, data := range testData {
partition, offset, err := p.SendData(testTopic, data)
if err != nil {
t.Log(err)
continue
}
t.Log("partition:", partition, "offset:", offset)
}
}
func TestInitAsyncProducer(t *testing.T) {
// Test InitAsyncProducer default options
p, err := InitAsyncProducer(addrs)
if err != nil {
t.Log(err)
}
// Test InitAsyncProducer with options
p, err = InitAsyncProducer(addrs,
AsyncProducerWithVersion(sarama.V3_6_0_0),
AsyncProducerWithClientID("my-client-id"),
AsyncProducerWithRequiredAcks(sarama.WaitForLocal),
AsyncProducerWithPartitioner(sarama.NewRandomPartitioner),
AsyncProducerWithReturnSuccesses(true),
AsyncProducerWithFlushMessages(100),
AsyncProducerWithFlushFrequency(time.Second),
AsyncProducerWithFlushBytes(16*1024),
AsyncProducerWithTLS(certfile.Path("two-way/server/server.pem"), certfile.Path("two-way/server/server.key"), certfile.Path("two-way/ca.pem"), true),
AsyncProducerWithZapLogger(zap.NewExample()),
AsyncProducerWithHandleFailed(func(msg *sarama.ProducerMessage) error {
t.Logf("handle failed message: %v", msg)
return nil
}),
)
if err != nil {
t.Log(err)
}
// Test InitAsyncProducer custom options
config := sarama.NewConfig()
config.Producer.Return.Successes = true
p, err = InitAsyncProducer(addrs, AsyncProducerWithConfig(config))
if err != nil {
t.Log(err)
return
}
time.Sleep(time.Second)
_ = p.Close()
}
func TestAsyncProducer_SendMessage(t *testing.T) {
p, err := InitAsyncProducer(addrs, AsyncProducerWithFlushFrequency(time.Millisecond*100))
if err != nil {
t.Log(err)
return
}
defer p.Close()
msg1 := &sarama.ProducerMessage{
Topic: testTopic,
Value: sarama.StringEncoder("hello world " + time.Now().String()),
}
msg2 := &sarama.ProducerMessage{
Topic: testTopic,
Value: sarama.StringEncoder("foo bar " + time.Now().String()),
}
err = p.SendMessage(msg1, msg2)
if err != nil {
t.Error(err)
return
}
time.Sleep(time.Millisecond * 200) // wait for messages to be sent, and flush them
}
func TestAsyncProducer_SendData(t *testing.T) {
p, err := InitAsyncProducer(addrs, AsyncProducerWithFlushFrequency(time.Millisecond*100))
if err != nil {
t.Log(err)
return
}
defer p.Close()
err = p.SendData(testTopic, testData...)
if err != nil {
t.Error(err)
return
}
time.Sleep(time.Millisecond * 200) // wait for messages to be sent, and flush them
}
func TestSyncProducer(t *testing.T) {
sp := mocks.NewSyncProducer(t, nil)
sp.ExpectSendMessageAndSucceed()
p := &SyncProducer{Producer: sp}
defer p.Close()
msg := testData[0].(*sarama.ProducerMessage)
partition, offset, err := p.SendMessage(msg)
if err != nil {
t.Log(err)
} else {
t.Log("partition:", partition, "offset:", offset)
}
for _, data := range testData {
sp.ExpectSendMessageAndSucceed()
p = &SyncProducer{Producer: sp}
partition, offset, err := p.SendData(testTopic, data)
if err != nil {
t.Log(err)
continue
} else {
t.Log("partition:", partition, "offset:", offset)
}
}
}
func TestAsyncProducer(t *testing.T) {
ap := mocks.NewAsyncProducer(t, nil)
ap.ExpectInputAndSucceed()
p := &AsyncProducer{Producer: ap, exit: make(chan struct{}), zapLogger: zap.NewExample()}
defer p.Close()
go p.handleResponse(nil)
msg := testData[0].(*sarama.ProducerMessage)
err := p.SendMessage(msg)
if err != nil {
t.Log(err)
} else {
t.Log("send message success")
}
for _, data := range testData {
ap.ExpectInputAndSucceed()
p.Producer = ap
err := p.SendData(testTopic, data)
if err != nil {
t.Log(err)
continue
} else {
t.Log("send message success")
}
}
}
package mgo
import (
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Model embedded structs, add `bson: ",inline"` when defining table structs
type Model struct {
ID primitive.ObjectID `bson:"_id" json:"id"`
CreatedAt time.Time `bson:"created_at" json:"createdAt"`
UpdatedAt time.Time `bson:"updated_at" json:"updatedAt"`
DeletedAt *time.Time `bson:"deleted_at,omitempty" json:"deletedAt,omitempty"`
}
// SetModelValue set model fields
func (p *Model) SetModelValue() {
now := time.Now()
if !p.ID.IsZero() {
p.ID = primitive.NewObjectID()
}
if p.CreatedAt.IsZero() {
p.CreatedAt = now
p.UpdatedAt = now
}
}
// ExcludeDeleted exclude soft deleted records
func ExcludeDeleted(filter bson.M) bson.M {
if filter == nil {
filter = bson.M{}
}
filter["deleted_at"] = bson.M{"$exists": false}
return filter
}
// EmbedUpdatedAt embed updated_at datetime column
func EmbedUpdatedAt(update bson.M) bson.M {
updateM := bson.M{}
if v, ok := update["$set"]; ok {
if m, ok2 := v.(bson.M); ok2 {
m["updated_at"] = time.Now()
updateM["$set"] = m
}
} else {
update["updated_at"] = time.Now()
updateM["$set"] = update
}
return updateM
}
// EmbedDeletedAt embed deleted_at datetime column
func EmbedDeletedAt(update bson.M) bson.M {
updateM := bson.M{}
if v, ok := update["$set"]; ok {
if m, ok2 := v.(bson.M); ok2 {
m["deleted_at"] = time.Now()
updateM["$set"] = m
}
} else {
updateM["$set"] = bson.M{"deleted_at": time.Now()}
}
return updateM
}
// ConvertToObjectIDs convert ids to objectIDs
func ConvertToObjectIDs(ids []string) []primitive.ObjectID {
oids := []primitive.ObjectID{}
for _, id := range ids {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
continue
}
oids = append(oids, oid)
}
return oids
}
// Package mgo is a library wrapped on go.mongodb.org/mongo-driver/mongo, with added features paging queries, etc.
package mgo
import (
"context"
"errors"
"net/url"
"strings"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
)
type Database = mongo.Database
var ErrNoDocuments = mongo.ErrNoDocuments
const (
// DBDriverName mongodb driver
DBDriverName = "mongodb"
)
// Init connecting to mongo
func Init(dsn string, opts ...*options.ClientOptions) (*mongo.Database, error) {
u, err := url.Parse(dsn)
if err != nil {
return nil, err
}
dbName := strings.TrimLeft(u.Path, "/")
if dbName == "" {
return nil, errors.New("database name is empty")
}
var uri string
if u.RawQuery == "" {
uri = strings.TrimRight(dsn, u.Path)
} else {
tmp := strings.TrimRight(dsn, u.RawQuery)
uri = strings.TrimRight(tmp, dbName+"?") + "?" + u.RawQuery
}
return Init2(uri, dbName, opts...)
}
// Init2 connecting to mongo using uri
func Init2(uri string, dbName string, opts ...*options.ClientOptions) (*mongo.Database, error) {
ctx := context.Background()
mongoOpts := []*options.ClientOptions{
options.Client().ApplyURI(uri),
}
mongoOpts = append(mongoOpts, opts...)
client, err := mongo.Connect(ctx, mongoOpts...)
if err != nil {
return nil, err
}
err = client.Ping(ctx, nil)
if err != nil {
return nil, err
}
db := client.Database(dbName)
return db, nil
}
// Close mongodb
func Close(db *mongo.Database) error {
return db.Client().Disconnect(context.Background())
}
// WithOption set option for mongodb
func WithOption() *options.ClientOptions {
return options.Client()
}
type customLogger struct {
zapLogger *zap.Logger
}
func (l *customLogger) Info(_ int, msg string, kvs ...interface{}) {
l.zapLogger.Info(msg, zap.String("msg", msg), zap.Any("kv", kvs))
}
func (l *customLogger) Error(err error, msg string, kvs ...interface{}) {
l.zapLogger.Warn(msg, zap.Error(err), zap.String("msg", msg), zap.Any("kv", kvs))
}
// NewCustomLogger create a custom logger for mongodb, debug level is used by default.
// example: WithOption().SetLoggerOptions(NewCustomLogger(logger.Get(), true))
func NewCustomLogger(l *zap.Logger, isDebugLevel bool) *options.LoggerOptions {
if l == nil {
l, _ = zap.NewProduction()
}
sink := &customLogger{zapLogger: l}
level := options.LogLevelInfo
if isDebugLevel {
level = options.LogLevelDebug
}
// Create a client with our logger options.
return options.
Logger().
SetSink(sink).
SetMaxDocumentLength(300).
SetComponentLevel(options.LogComponentCommand, level)
}
package mgo
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
func TestInit(t *testing.T) {
dsns := []string{
"mongodb://root:123456@192.168.3.37:27017/account",
"mongodb://root:123456@192.168.3.37:27017/account?connectTimeoutMS=2000",
"mongodb://root:123456@192.168.3.37:27017/account?socketTimeoutMS=30000&maxPoolSize=100&minPoolSize=1&maxConnIdleTimeMS=300000",
// error
"mongodb-dsn",
"mongodb://root:123456@192.168.3.37",
}
for _, dsn := range dsns {
db, err := Init(dsn, WithOption().SetConnectTimeout(2*time.Second))
if err != nil {
t.Log(err)
continue
}
time.Sleep(time.Millisecond * 100)
defer Close(db)
}
defer func() { recover() }()
db := &mongo.Database{}
_ = Close(db)
}
func TestInit2(t *testing.T) {
uri := "mongodb://root:123456@192.168.3.37:27017"
dbName := "account"
db, err := Init2(uri, dbName,
WithOption().SetConnectTimeout(2*time.Second),
WithOption().SetLoggerOptions(NewCustomLogger(nil, true)),
)
if err != nil {
t.Log(err)
return
}
time.Sleep(time.Millisecond * 100)
defer Close(db)
}
func TestModel_SetModelValue(t *testing.T) {
m := new(Model)
m.SetModelValue()
assert.NotNil(t, m.ID)
assert.NotNil(t, m.CreatedAt)
assert.NotNil(t, m.UpdatedAt)
}
func TestExcludeDeleted(t *testing.T) {
filter := bson.M{"foo": "bar"}
filter = ExcludeDeleted(filter)
assert.NotNil(t, filter["deleted_at"])
filter = ExcludeDeleted(nil)
assert.NotNil(t, filter["deleted_at"])
}
func TestEmbedUpdatedAt(t *testing.T) {
update := bson.M{"$set": bson.M{"foo": "bar"}}
update = EmbedUpdatedAt(update)
m := update["$set"].(bson.M)
assert.NotNil(t, m["updated_at"])
update = bson.M{"foo": "bar"}
update = EmbedUpdatedAt(update)
m = update["$set"].(bson.M)
assert.NotNil(t, m["updated_at"])
}
func TestEmbedDeletedAt(t *testing.T) {
update := bson.M{"$set": bson.M{"foo": "bar"}}
update = EmbedDeletedAt(update)
m := update["$set"].(bson.M)
assert.NotNil(t, m["deleted_at"])
update = bson.M{"foo": "bar"}
update = EmbedDeletedAt(update)
m = update["$set"].(bson.M)
assert.NotNil(t, m["deleted_at"])
}
func TestConvertToObjectIDs(t *testing.T) {
ids := []string{"65c9ae1b1378ae7f0787a039", "invalid_id"}
oids := ConvertToObjectIDs(ids)
assert.Equal(t, len(oids), 1)
}
func Test_customLogger(t *testing.T) {
l, _ := zap.NewProduction()
logger := &customLogger{l}
logger.Info(0, "foo", map[string]interface{}{"bar": "baz"})
logger.Error(errors.New("error"), "foo", map[string]interface{}{"bar": "baz"})
}
package query
import (
"strings"
"go.mongodb.org/mongo-driver/bson"
)
var defaultMaxSize = 1000
const oidName = "_id"
// SetMaxSize change the default maximum number of pages per page
func SetMaxSize(max int) {
if max < 10 {
max = 10
}
defaultMaxSize = max
}
// Page info
type Page struct {
page int // page number, starting from page 0
limit int // number per page
// sort fields, default is id backwards, you can add - sign before the field to indicate
// reverse order, no - sign to indicate ascending order, multiple fields separated by comma
sort bson.D
}
// Page get page value
func (p *Page) Page() int {
return p.page
}
// Limit number per page
func (p *Page) Limit() int {
return p.limit
}
// Size number per page
// Deprecated: use Limit instead, will delete it in the future
func (p *Page) Size() int {
return p.limit
}
// Sort get sort field
func (p *Page) Sort() bson.D {
return p.sort
}
// Skip get offset value
func (p *Page) Skip() int {
return p.page * p.limit
}
// DefaultPage default page, number 20 per page, sorted by id backwards
func DefaultPage(page int) *Page {
if page < 0 {
page = 0
}
return &Page{
page: page,
limit: 10,
sort: bson.D{{oidName, -1}}, //nolint
}
}
// NewPage custom page, starting from page 0.
// the parameter columnNames indicates a sort field, if empty means id descending, if there are multiple column names, separated by a comma,
// a '-' sign in front of each column name indicates descending order, otherwise ascending order.
func NewPage(page int, limit int, columnNames string) *Page {
if page < 0 {
page = 0
}
if limit > defaultMaxSize {
limit = defaultMaxSize
} else if limit < 1 {
limit = 10
}
return &Page{
page: page,
limit: limit,
sort: getSort(columnNames),
}
}
// convert to mysql sort, each column name preceded by a '-' sign, indicating descending order, otherwise ascending order, example:
//
// columnNames="name" means sort by name in ascending order,
// columnNames="-name" means sort by name descending,
// columnNames="name,age" means sort by name in ascending order, otherwise sort by age in ascending order,
// columnNames="-name,-age" means sort by name descending before sorting by age descending.
func getSort(columnNames string) bson.D {
d := bson.D{}
columnNames = strings.Replace(columnNames, " ", "", -1)
if columnNames == "" {
d = bson.D{{oidName, -1}} //nolint
return d
}
names := strings.Split(columnNames, ",")
for _, name := range names {
if name[0] == '-' && len(name) > 1 {
col := name[1:]
if col == "id" {
col = oidName
}
d = append(d, bson.E{col, -1}) //nolint
} else {
if name == "id" {
name = oidName
}
d = append(d, bson.E{name, 1}) //nolint
}
}
return d
}
// Package query is a library of custom condition queries, support for complex conditional paging queries.
package query
import (
"fmt"
"regexp"
"strings"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
// Eq equal
Eq = "eq"
eqSymbol = "="
// Neq not equal
Neq = "neq"
neqSymbol = "!="
// Gt greater than
Gt = "gt"
gtSymbol = ">"
// Gte greater than or equal
Gte = "gte"
gteSymbol = ">="
// Lt less than
Lt = "lt"
ltSymbol = "<"
// Lte less than or equal
Lte = "lte"
lteSymbol = "<="
// Like fuzzy lookup
Like = "like"
// In include
In = "in"
// AND logic and
AND string = "and" //nolint
andSymbol1 = "&"
andSymbol2 = "&&"
// OR logic or
OR string = "or" //nolint
orSymbol1 = "|"
orSymbol2 = "||"
allLogicAnd = 1
allLogicOr = 2
)
var expMap = map[string]string{
Eq: eqSymbol,
eqSymbol: eqSymbol,
Neq: neqSymbol,
neqSymbol: neqSymbol,
Gt: gtSymbol,
gtSymbol: gtSymbol,
Gte: gteSymbol,
gteSymbol: gteSymbol,
Lt: ltSymbol,
ltSymbol: ltSymbol,
Lte: lteSymbol,
lteSymbol: lteSymbol,
Like: Like,
In: In,
}
var logicMap = map[string]string{
AND: andSymbol1,
andSymbol1: andSymbol1,
andSymbol2: andSymbol1,
OR: orSymbol1,
orSymbol1: orSymbol1,
orSymbol2: orSymbol1,
}
// Params query parameters
type Params struct {
Page int `json:"page" form:"page" binding:"gte=0"`
Limit int `json:"limit" form:"limit" binding:"gte=1"`
Sort string `json:"sort,omitempty" form:"sort" binding:""`
Columns []Column `json:"columns,omitempty" form:"columns"` // not required
// Deprecated: use Limit instead in xmall version v1.8.6, will remove in the future
Size int `json:"size" form:"size"`
}
// Column query info
type Column struct {
Name string `json:"name" form:"name"` // column name
Exp string `json:"exp" form:"exp"` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
Value interface{} `json:"value" form:"value"` // column value
Logic string `json:"logic" form:"logic"` // logical type, defaults to and when the value is null, with &(and), ||(or)
}
func (c *Column) checkValid() error {
if c.Name == "" {
return fmt.Errorf("field 'name' cannot be empty")
}
if c.Value == nil {
return fmt.Errorf("field 'value' cannot be nil")
}
return nil
}
func (c *Column) convertLogic() error {
if c.Logic == "" {
c.Logic = AND
}
if v, ok := logicMap[strings.ToLower(c.Logic)]; ok { //nolint
c.Logic = v
return nil
}
return fmt.Errorf("unknown logic type '%s'", c.Logic)
}
// converting ExpType to sql expressions and LogicType to sql using characters
func (c *Column) convert() error {
if err := c.checkValid(); err != nil {
return err
}
if c.Name == "id" || c.Name == "_id" {
if str, ok := c.Value.(string); ok {
c.Name = "_id"
c.Value, _ = primitive.ObjectIDFromHex(str)
}
} else if strings.Contains(c.Name, ":oid") {
if str, ok := c.Value.(string); ok {
c.Name = strings.Replace(c.Name, ":oid", "", 1)
c.Value, _ = primitive.ObjectIDFromHex(str)
}
}
if c.Exp == "" {
c.Exp = Eq
}
if v, ok := expMap[strings.ToLower(c.Exp)]; ok { //nolint
c.Exp = v
switch c.Exp {
//case eqSymbol:
case neqSymbol:
c.Value = bson.M{"$neq": c.Value}
case gtSymbol:
c.Value = bson.M{"$gt": c.Value}
case gteSymbol:
c.Value = bson.M{"$gte": c.Value}
case ltSymbol:
c.Value = bson.M{"$lt": c.Value}
case lteSymbol:
c.Value = bson.M{"$lte": c.Value}
case Like:
escapedValue := regexp.QuoteMeta(fmt.Sprintf("%v", c.Value))
c.Value = bson.M{"$regex": escapedValue, "$options": "i"}
case In:
val, ok := c.Value.(string)
if !ok {
return fmt.Errorf("invalid value type '%s'", c.Value)
}
values := []interface{}{}
ss := strings.Split(val, ",")
for _, s := range ss {
values = append(values, s)
}
c.Value = bson.M{"$in": values}
}
} else {
return fmt.Errorf("unknown exp type '%s'", c.Exp)
}
return c.convertLogic()
}
// ConvertToPage converted to page
func (p *Params) ConvertToPage() (sort bson.D, limit int, skip int) { //nolint
page := NewPage(p.Page, p.Limit, p.Sort)
sort = page.sort
limit = page.limit
skip = page.page * page.limit
return //nolint
}
// ConvertToMongoFilter conversion to mongo-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (p *Params) ConvertToMongoFilter() (bson.M, error) {
filter := bson.M{}
l := len(p.Columns)
switch l {
case 0:
return bson.M{}, nil
case 1: // l == 1
err := p.Columns[0].convert()
if err != nil {
return nil, err
}
filter[p.Columns[0].Name] = p.Columns[0].Value
return filter, nil
case 2: // l == 2
err := p.Columns[0].convert()
if err != nil {
return nil, err
}
err = p.Columns[1].convert()
if err != nil {
return nil, err
}
if p.Columns[0].Logic == andSymbol1 {
filter = bson.M{"$and": []bson.M{
{p.Columns[0].Name: p.Columns[0].Value},
{p.Columns[1].Name: p.Columns[1].Value}}}
} else {
filter = bson.M{"$or": []bson.M{
{p.Columns[0].Name: p.Columns[0].Value},
{p.Columns[1].Name: p.Columns[1].Value}}}
}
return filter, nil
default: // l >=3
return p.convertMultiColumns()
}
}
func (p *Params) convertMultiColumns() (bson.M, error) {
filter := bson.M{}
logicType, groupIndexes, err := checkSameLogic(p.Columns)
if err != nil {
return nil, err
}
if logicType == allLogicAnd {
for _, column := range p.Columns {
err := column.convert()
if err != nil {
return nil, err
}
if v, ok := filter["$and"]; !ok {
filter["$and"] = []bson.M{{column.Name: column.Value}}
} else {
if cols, ok1 := v.([]bson.M); ok1 {
cols = append(cols, bson.M{column.Name: column.Value})
filter["$and"] = cols
}
}
}
return filter, nil
} else if logicType == allLogicOr {
for _, column := range p.Columns {
err := column.convert()
if err != nil {
return nil, err
}
if v, ok := filter["$or"]; !ok {
filter["$or"] = []bson.M{{column.Name: column.Value}}
} else {
if cols, ok1 := v.([]bson.M); ok1 {
cols = append(cols, bson.M{column.Name: column.Value})
filter["$or"] = cols
}
}
}
return filter, nil
}
orConditions := []bson.M{}
for _, indexes := range groupIndexes {
if len(indexes) == 1 {
column := p.Columns[indexes[0]]
err := column.convert()
if err != nil {
return nil, err
}
orConditions = append(orConditions, bson.M{column.Name: column.Value})
} else {
andConditions := []bson.M{}
for _, index := range indexes {
column := p.Columns[index]
err := column.convert()
if err != nil {
return nil, err
}
andConditions = append(andConditions, bson.M{column.Name: column.Value})
}
orConditions = append(orConditions, bson.M{"$and": andConditions})
}
}
filter["$or"] = orConditions
return filter, nil
}
func checkSameLogic(columns []Column) (int, [][]int, error) {
orIndexes := []int{}
l := len(columns)
for i, column := range columns {
if i == l-1 { // ignore the logical type of the last column
break
}
err := column.convertLogic()
if err != nil {
return 0, nil, err
}
if column.Logic == orSymbol1 {
orIndexes = append(orIndexes, i)
}
}
if len(orIndexes) == 0 {
return allLogicAnd, nil, nil
} else if len(orIndexes) == l-1 {
return allLogicOr, nil, nil
}
// mix and or
groupIndexes := groupingIndex(l, orIndexes)
return 0, groupIndexes, nil
}
func groupingIndex(l int, orIndexes []int) [][]int {
groupIndexes := [][]int{}
lastIndex := 0
for _, index := range orIndexes {
group := []int{}
for i := lastIndex; i <= index; i++ {
group = append(group, i)
}
groupIndexes = append(groupIndexes, group)
if lastIndex == index {
lastIndex++
} else {
lastIndex = index
}
}
group := []int{}
for i := lastIndex + 1; i < l; i++ {
group = append(group, i)
}
groupIndexes = append(groupIndexes, group)
return groupIndexes
}
// Conditions query conditions
type Conditions struct {
Columns []Column `json:"columns" form:"columns" binding:"min=1"` // columns info
}
// CheckValid check valid
func (c *Conditions) CheckValid() error {
if len(c.Columns) == 0 {
return fmt.Errorf("field 'columns' cannot be empty")
}
for _, column := range c.Columns {
err := column.checkValid()
if err != nil {
return err
}
if column.Exp != "" {
if _, ok := expMap[column.Exp]; !ok {
return fmt.Errorf("unknown exp type '%s'", column.Exp)
}
}
if column.Logic != "" {
if _, ok := logicMap[column.Logic]; !ok {
return fmt.Errorf("unknown logic type '%s'", column.Logic)
}
}
}
return nil
}
// ConvertToMongo conversion to mongo-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (c *Conditions) ConvertToMongo() (bson.M, error) {
p := &Params{Columns: c.Columns}
return p.ConvertToMongoFilter()
}
package query
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func TestPage(t *testing.T) {
page := DefaultPage(-1)
t.Log(page.Page(), page.Limit(), page.Sort(), page.Skip())
page = NewPage(0, 20, "")
t.Log(page.Page(), page.Limit(), page.Sort(), page.Skip())
SetMaxSize(1)
page = NewPage(0, 20, "_id")
t.Log(page.Page(), page.Limit(), page.Sort(), page.Skip())
}
func TestParams_ConvertToPage(t *testing.T) {
p := &Params{
Page: 0,
Limit: 20,
Sort: "age,-name",
}
order, limit, offset := p.ConvertToPage()
t.Logf("order=%v, limit=%d, skip=%d", order, limit, offset)
}
func TestParams_ConvertToMongoFilter(t *testing.T) {
type args struct {
columns []Column
}
tests := []struct {
name string
args args
want bson.M
wantErr bool
}{
// --------------------------- only 1 column query ------------------------------
{
name: "1 column eq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
},
},
want: bson.M{"name": "ZhangSan"},
wantErr: false,
},
{
name: "1 column neq",
args: args{
columns: []Column{
{
Name: "name",
Exp: "!=",
Value: "ZhangSan",
},
},
},
want: bson.M{"name": bson.M{"$neq": "ZhangSan"}},
wantErr: false,
},
{
name: "1 column gt",
args: args{
columns: []Column{
{
Name: "age",
Exp: ">",
Value: 20,
},
},
},
want: bson.M{"age": bson.M{"$gt": 20}},
wantErr: false,
},
{
name: "1 column gte",
args: args{
columns: []Column{
{
Name: "age",
Exp: ">=",
Value: 20,
},
},
},
want: bson.M{"age": bson.M{"$gte": 20}},
wantErr: false,
},
{
name: "1 column lt",
args: args{
columns: []Column{
{
Name: "age",
Exp: "<",
Value: 20,
},
},
},
want: bson.M{"age": bson.M{"$lt": 20}},
wantErr: false,
},
{
name: "1 column lte",
args: args{
columns: []Column{
{
Name: "age",
Exp: "<=",
Value: 20,
},
},
},
want: bson.M{"age": bson.M{"$lte": 20}},
wantErr: false,
},
{
name: "1 column like",
args: args{
columns: []Column{
{
Name: "name",
Exp: Like,
Value: "Li",
},
},
},
want: bson.M{"name": bson.M{"$options": "i", "$regex": "Li"}},
wantErr: false,
},
{
name: "1 column IN",
args: args{
columns: []Column{
{
Name: "name",
Exp: In,
Value: "ab,cd,ef",
},
},
},
want: bson.M{"name": bson.M{"$in": []interface{}{"ab", "cd", "ef"}}},
wantErr: false,
},
// --------------------------- query 2 columns ------------------------------
{
name: "2 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
},
},
want: bson.M{"$and": []bson.M{{"name": "ZhangSan"}, {"gender": "male"}}},
wantErr: false,
},
{
name: "2 columns neq and",
args: args{
columns: []Column{
{
Name: "name",
Exp: "!=",
Value: "ZhangSan",
},
{
Name: "name",
Exp: "!=",
Value: "LiSi",
},
},
},
want: bson.M{"$and": []bson.M{{"name": bson.M{"$neq": "ZhangSan"}}, {"name": bson.M{"$neq": "LiSi"}}}},
wantErr: false,
},
{
name: "2 columns gt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Exp: ">",
Value: 20,
},
},
},
want: bson.M{"$and": []bson.M{{"gender": "male"}, {"age": bson.M{"$gt": 20}}}},
wantErr: false,
},
{
name: "2 columns gte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Exp: ">=",
Value: 20,
},
},
},
want: bson.M{"$and": []bson.M{{"gender": "male"}, {"age": bson.M{"$gte": 20}}}},
wantErr: false,
},
{
name: "2 columns lt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Exp: "<",
Value: 20,
},
},
},
want: bson.M{"$and": []bson.M{{"gender": "female"}, {"age": bson.M{"$lt": 20}}}},
wantErr: false,
},
{
name: "2 columns lte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Exp: "<=",
Value: 20,
},
},
},
want: bson.M{"$and": []bson.M{{"gender": "female"}, {"age": bson.M{"$lte": 20}}}},
wantErr: false,
},
{
name: "2 columns range and",
args: args{
columns: []Column{
{
Name: "age",
Exp: ">=",
Value: 10,
},
{
Name: "age",
Exp: "<=",
Value: 20,
},
},
},
want: bson.M{"$and": []bson.M{{"age": bson.M{"$gte": 10}}, {"age": bson.M{"$lte": 20}}}},
wantErr: false,
},
{
name: "2 columns eq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
Logic: "||",
},
{
Name: "gender",
Value: "female",
},
},
},
want: bson.M{"$or": []bson.M{{"name": "LiSi"}, {"gender": "female"}}},
wantErr: false,
},
{
name: "2 columns neq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
Logic: "||",
},
{
Name: "gender",
Exp: "!=",
Value: "male",
},
},
},
want: bson.M{"$or": []bson.M{{"name": "LiSi"}, {"gender": bson.M{"$neq": "male"}}}},
wantErr: false,
},
{
name: "2 columns eq and in",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "name",
Exp: In,
Value: "LiSi,ZhangSan,WangWu",
},
},
},
want: bson.M{"$and": []bson.M{{"gender": "male"}, {"name": bson.M{"$in": []interface{}{"LiSi", "ZhangSan", "WangWu"}}}}},
wantErr: false,
},
// --------------------------- query 3 columns ------------------------------
{
name: "3 columns and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "age",
Exp: "<",
Value: 12,
},
},
},
want: bson.M{"$and": []bson.M{{"gender": "male"}, {"name": "ZhangSan"}, {"age": bson.M{"$lt": 12}}}},
wantErr: false,
},
{
name: "3 columns or",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "||",
},
{
Name: "name",
Value: "ZhangSan",
Logic: "||",
},
{
Name: "age",
Exp: "<",
Value: 12,
},
},
},
want: bson.M{"$or": []bson.M{{"gender": "male"}, {"name": "ZhangSan"}, {"age": bson.M{"$lt": 12}}}},
wantErr: false,
},
{
name: "3 columns mix (or and)",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "and",
},
{
Name: "name",
Value: "ZhangSan",
Logic: "||",
},
{
Name: "age",
Exp: "<",
Value: 12,
},
},
},
want: bson.M{"$or": []bson.M{{"$and": []bson.M{{"gender": "male"}, {"name": "ZhangSan"}}}, {"age": bson.M{"$lt": 12}}}},
wantErr: false,
},
// --------------------------- query 4 columns ------------------------------
{
name: "4 columns mix (or and)",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "||",
},
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "age",
Exp: "<",
Value: 12,
Logic: "||",
},
{
Name: "city",
Value: "canton",
},
},
},
want: bson.M{"$or": []bson.M{{"gender": "male"}, {"$and": []bson.M{{"name": "ZhangSan"}, {"age": bson.M{"$lt": 12}}}}, {"city": "canton"}}},
wantErr: false,
},
{
name: "convert to object id",
args: args{
columns: []Column{
{
Name: "id",
Value: "65ce48483f11aff697e30d6d",
},
{
Name: "order_id:oid",
Value: "65ce48483f11aff697e30d6d",
},
},
},
want: bson.M{"$and": []bson.M{{"_id": primitive.ObjectID{0x65, 0xce, 0x48, 0x48, 0x3f, 0x11, 0xaf, 0xf6, 0x97, 0xe3, 0xd, 0x6d}}, {"order_id": primitive.ObjectID{0x65, 0xce, 0x48, 0x48, 0x3f, 0x11, 0xaf, 0xf6, 0x97, 0xe3, 0xd, 0x6d}}}},
wantErr: false,
},
// ---------------------------- error ----------------------------------------------
{
name: "exp type err",
args: args{
columns: []Column{
{
Name: "gender",
Exp: "xxxxxx",
Value: "male",
},
},
},
want: nil,
wantErr: true,
},
{
name: "logic type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "xxxxxx",
},
},
},
want: nil,
wantErr: true,
},
{
name: "name empty",
args: args{
columns: []Column{
{
Name: "",
Value: "male",
},
},
},
want: nil,
wantErr: true,
},
{
name: "value empty",
args: args{
columns: []Column{
{
Name: "name",
Value: nil,
},
},
},
want: nil,
wantErr: true,
},
{
name: "empty",
args: args{
columns: nil,
},
want: primitive.M{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := &Params{
Columns: tt.args.columns,
}
got, err := params.ConvertToMongoFilter()
if (err != nil) != tt.wantErr {
t.Errorf("ConvertToMongoFilter() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ConvertToMongoFilter() got = %#v, want = %#v", got, tt.want)
}
})
}
}
func TestConditions_ConvertToMongo(t *testing.T) {
c := Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
got, err := c.ConvertToMongo()
if err != nil {
t.Error(err)
}
want := bson.M{"$and": []bson.M{{"name": "ZhangSan"}, {"gender": "male"}}}
if !reflect.DeepEqual(got, want) {
t.Errorf("ConvertToMongo() got = %+v, want %+v", got, want)
}
}
func TestConditions_checkValid(t *testing.T) {
// empty error
c := Conditions{}
err := c.CheckValid()
assert.Error(t, err)
// value is empty error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: nil,
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// exp error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Exp: "unknown-exp",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// logic error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Logic: "unknown-logic",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// success
c = Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
err = c.CheckValid()
assert.NoError(t, err)
}
func Test_groupingIndex(t *testing.T) {
type args struct {
l int
orIndexes []int
}
tests := []struct {
name string
args args
want [][]int
}{
{
name: "4 index 1",
args: args{
l: 4,
orIndexes: []int{0, 2},
},
want: [][]int{{0}, {1, 2}, {3}},
},
{
name: "4 index 2",
args: args{
l: 4,
orIndexes: []int{1},
},
want: [][]int{{0, 1}, {2, 3}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := groupingIndex(tt.args.l, tt.args.orIndexes)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("groupingIndex got = %#v, want = %#v", got, tt.want)
}
t.Log(got)
})
}
}
func Test_getSort(t *testing.T) {
names := []string{
"", "id", "-id", "gender", "gender,id", "-gender,-id",
}
for _, name := range names {
d := getSort(name)
t.Log(d)
}
}
// Package nacoscli provides for getting the configuration from the nacos configuration center and parse it into a structure.
package nacoscli
import (
"errors"
"fmt"
"gitlab.wanzhuangkj.com/tush/xpkg/xerrors/xerror"
"os"
"strings"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
"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"
)
// Params nacos parameters
type Params struct {
IPAddr string // server address
Port uint64 // port
Scheme string // http or grpc
ContextPath string // path
// if you set this parameter, the above fields(IPAddr, Port, Scheme, ContextPath) are invalid
serverConfigs []constant.ServerConfig
NamespaceID string // namespace id
// if you set this parameter, the above field(NamespaceID) is invalid
clientConfig *constant.ClientConfig
Group string // group, example: dev, prod, test
DataID string // config file id
Format string // configuration file type: json,yaml,toml
}
func (p *Params) valid() error {
if p.Group == "" {
return errors.New("field 'Group' cannot be empty")
}
if p.DataID == "" {
return errors.New("field 'DataID' cannot be empty")
}
if p.Format == "" {
return errors.New("field 'DataID' cannot be empty")
}
format := strings.ToLower(p.Format)
switch format {
case "json", "yaml", "toml":
p.Format = format
case "yml":
p.Format = "yaml"
default:
return fmt.Errorf("config file types 'Format=%s' not supported", p.Format)
}
return nil
}
func setParams(params *Params, opts ...Option) {
o := defaultOptions()
o.apply(opts...)
params.clientConfig = o.clientConfig
params.serverConfigs = o.serverConfigs
// create clientConfig
if params.clientConfig == nil {
params.clientConfig = &constant.ClientConfig{
NamespaceId: params.NamespaceID,
TimeoutMs: 5000,
NotLoadCacheAtStart: true,
LogDir: os.TempDir() + "/nacos/log",
CacheDir: os.TempDir() + "/nacos/cache",
Username: o.username,
Password: o.password,
}
}
// create serverConfig
if params.serverConfigs == nil {
params.serverConfigs = []constant.ServerConfig{
{
IpAddr: params.IPAddr,
Port: params.Port,
Scheme: params.Scheme,
ContextPath: params.ContextPath,
},
}
}
}
// GetConfig get configuration from nacos
func GetConfig(params *Params, opts ...Option) (string, []byte, error) {
err := params.valid()
if err != nil {
return "", nil, err
}
setParams(params, opts...)
// create a dynamic configuration client
configClient, err := clients.NewConfigClient(
vo.NacosClientParam{
ClientConfig: params.clientConfig,
ServerConfigs: params.serverConfigs,
},
)
if err != nil {
return "", nil, err
}
// read config content
data, err := configClient.GetConfig(vo.ConfigParam{
DataId: params.DataID,
Group: params.Group,
})
if err != nil {
return "", nil, err
}
return params.Format, []byte(data), err
}
// Init get configuration from nacos and parse to struct, use for configuration center
//
// Deprecated: use GetConfig instead.
func Init(_ interface{}, _ *Params, _ ...Option) error {
return errors.New("not implemented, use GetConfig instead")
}
// NewNamingClient create a service registration and discovery of nacos client.
// Note: If parameter WithClientConfig is set, nacosNamespaceID is invalid,
// if parameter WithServerConfigs is set, nacosIPAddr and nacosPort are invalid.
func NewNamingClient(nacosIPAddr string, nacosPort int, nacosNamespaceID string, opts ...Option) (naming_client.INamingClient, error) {
params := &Params{
IPAddr: nacosIPAddr,
Port: uint64(nacosPort),
NamespaceID: nacosNamespaceID,
}
setParams(params, opts...)
client, err := clients.NewNamingClient(
vo.NacosClientParam{
ClientConfig: params.clientConfig,
ServerConfigs: params.serverConfigs,
},
)
if err != nil {
return nil, xerror.New(err.Error())
}
return client, nil
}
package nacoscli
import (
"context"
"os"
"testing"
"time"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/stretchr/testify/assert"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
var (
ipAddr = "192.168.3.37"
port = 8848
namespaceID = "3454d2b5-2455-4d0e-bf6d-e033b086bb4c"
)
func TestParse(t *testing.T) {
//conf := new(map[string]interface{})
params := &Params{
IPAddr: ipAddr,
Port: uint64(port),
NamespaceID: namespaceID,
Group: "dev",
DataID: "serverNameExample.yml",
Format: "yaml",
}
utils.SafeRunWithTimeout(time.Second*2, func(cancel context.CancelFunc) {
format, data, err := GetConfig(params)
t.Log(err, format, data)
})
//conf = new(map[string]interface{})
params = &Params{
Group: "dev",
DataID: "serverNameExample.yml",
Format: "yaml",
}
clientConfig := &constant.ClientConfig{
NamespaceId: namespaceID,
TimeoutMs: 1000,
NotLoadCacheAtStart: true,
LogDir: os.TempDir() + "/nacos/log",
CacheDir: os.TempDir() + "/nacos/cache",
}
serverConfigs := []constant.ServerConfig{
{
IpAddr: ipAddr,
Port: uint64(port),
},
}
utils.SafeRunWithTimeout(time.Second*2, func(cancel context.CancelFunc) {
format, data, err := GetConfig(params,
WithClientConfig(clientConfig),
WithServerConfigs(serverConfigs),
WithAuth("foo", "bar"),
)
t.Log(err, format, data)
})
}
func TestNewNamingClient(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*2, func(cancel context.CancelFunc) {
namingClient, err := NewNamingClient(ipAddr, port, namespaceID)
t.Log(err, namingClient)
})
}
func TestError(t *testing.T) {
p := &Params{}
p.Group = ""
err := p.valid()
assert.Error(t, err)
p.Group = "group"
p.DataID = ""
err = p.valid()
assert.Error(t, err)
p.Group = "group"
p.DataID = "id"
p.Format = ""
err = p.valid()
assert.Error(t, err)
p.Group = "group"
p.DataID = "id"
p.Format = "yml"
err = p.valid()
assert.NoError(t, err)
p.Group = "group"
p.DataID = "id"
p.Format = "unknown"
err = p.valid()
assert.Error(t, err)
_, _, err = GetConfig(&Params{})
assert.Error(t, err)
}
package nacoscli
import (
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
)
type options struct {
username string
password string
// if set the clientConfig, the above fields(username, password) are invalid
clientConfig *constant.ClientConfig
serverConfigs []constant.ServerConfig
}
func defaultOptions() *options {
return &options{
clientConfig: nil,
serverConfigs: nil,
}
}
// Option set the nacos client options.
type Option func(*options)
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithAuth set authentication
func WithAuth(username string, password string) Option {
return func(o *options) {
o.username = username
o.password = password
}
}
// WithClientConfig set nacos client config
func WithClientConfig(clientConfig *constant.ClientConfig) Option {
return func(o *options) {
o.clientConfig = clientConfig
}
}
// WithServerConfigs set nacos server config
func WithServerConfigs(serverConfigs []constant.ServerConfig) Option {
return func(o *options) {
o.serverConfigs = serverConfigs
}
}
package rabbitmq
import amqp "github.com/rabbitmq/amqp091-go"
// ErrClosed closed
var ErrClosed = amqp.ErrClosed
const (
exchangeTypeDirect = "direct"
exchangeTypeTopic = "topic"
exchangeTypeFanout = "fanout"
exchangeTypeHeaders = "headers"
exchangeTypeDelayedMessage = "x-delayed-message"
// HeadersTypeAll all
HeadersTypeAll HeadersType = "all"
// HeadersTypeAny any
HeadersTypeAny HeadersType = "any"
)
// HeadersType headers type
type HeadersType = string
// Exchange rabbitmq minimum management unit
type Exchange struct {
name string // exchange name
eType string // exchange type: direct, topic, fanout, headers, x-delayed-message
routingKey string // route key
headersKeys map[string]interface{} // this field is required if eType=headers.
delayedMessageType string // this field is required if eType=headers, support direct, topic, fanout, headers
}
// Name exchange name
func (e *Exchange) Name() string {
return e.name
}
// Type exchange type
func (e *Exchange) Type() string {
return e.eType
}
// RoutingKey exchange routing key
func (e *Exchange) RoutingKey() string {
return e.routingKey
}
// HeadersKeys exchange headers keys
func (e *Exchange) HeadersKeys() map[string]interface{} {
return e.headersKeys
}
// DelayedMessageType exchange delayed message type
func (e *Exchange) DelayedMessageType() string {
return e.delayedMessageType
}
// NewDirectExchange create a direct exchange
func NewDirectExchange(exchangeName string, routingKey string) *Exchange {
return &Exchange{
name: exchangeName,
eType: exchangeTypeDirect,
routingKey: routingKey,
}
}
// NewTopicExchange create a topic exchange
func NewTopicExchange(exchangeName string, routingKey string) *Exchange {
return &Exchange{
name: exchangeName,
eType: exchangeTypeTopic,
routingKey: routingKey,
}
}
// NewFanoutExchange create a fanout exchange
func NewFanoutExchange(exchangeName string) *Exchange {
return &Exchange{
name: exchangeName,
eType: exchangeTypeFanout,
routingKey: "",
}
}
// NewHeadersExchange create a headers exchange, the headerType supports "all" and "any"
func NewHeadersExchange(exchangeName string, headersType HeadersType, keys map[string]interface{}) *Exchange {
if keys == nil {
keys = make(map[string]interface{})
}
switch headersType {
case HeadersTypeAll, HeadersTypeAny:
keys["x-match"] = headersType
default:
keys["x-match"] = HeadersTypeAll
}
return &Exchange{
name: exchangeName,
eType: exchangeTypeHeaders,
routingKey: "",
headersKeys: keys,
}
}
// NewDelayedMessageExchange create a delayed message exchange
func NewDelayedMessageExchange(exchangeName string, e *Exchange) *Exchange {
return &Exchange{
name: exchangeName,
eType: "x-delayed-message",
routingKey: e.routingKey,
delayedMessageType: e.eType,
headersKeys: e.headersKeys,
}
}
// -------------------------------------------------------------------------------------------
// QueueDeclareOption declare queue option.
type QueueDeclareOption func(*queueDeclareOptions)
type queueDeclareOptions struct {
autoDelete bool // delete automatically
exclusive bool // exclusive (only available to the program that created it)
noWait bool // block processing
args amqp.Table // additional properties
}
func (o *queueDeclareOptions) apply(opts ...QueueDeclareOption) {
for _, opt := range opts {
opt(o)
}
}
// default queue declare settings
func defaultQueueDeclareOptions() *queueDeclareOptions {
return &queueDeclareOptions{
autoDelete: false,
exclusive: false,
noWait: false,
args: nil,
}
}
// WithQueueDeclareAutoDelete set queue declare auto delete option.
func WithQueueDeclareAutoDelete(enable bool) QueueDeclareOption {
return func(o *queueDeclareOptions) {
o.autoDelete = enable
}
}
// WithQueueDeclareExclusive set queue declare exclusive option.
func WithQueueDeclareExclusive(enable bool) QueueDeclareOption {
return func(o *queueDeclareOptions) {
o.exclusive = enable
}
}
// WithQueueDeclareNoWait set queue declare no wait option.
func WithQueueDeclareNoWait(enable bool) QueueDeclareOption {
return func(o *queueDeclareOptions) {
o.noWait = enable
}
}
// WithQueueDeclareArgs set queue declare args option.
func WithQueueDeclareArgs(args map[string]interface{}) QueueDeclareOption {
return func(o *queueDeclareOptions) {
o.args = args
}
}
// -------------------------------------------------------------------------------------------
// ExchangeDeclareOption declare exchange option.
type ExchangeDeclareOption func(*exchangeDeclareOptions)
type exchangeDeclareOptions struct {
autoDelete bool // delete automatically
internal bool // public or not, false means public
noWait bool // block processing
args amqp.Table // additional properties
}
func (o *exchangeDeclareOptions) apply(opts ...ExchangeDeclareOption) {
for _, opt := range opts {
opt(o)
}
}
// default exchange declare settings
func defaultExchangeDeclareOptions() *exchangeDeclareOptions {
return &exchangeDeclareOptions{
//durable: true,
autoDelete: false,
internal: false,
noWait: false,
args: nil,
}
}
// WithExchangeDeclareAutoDelete set exchange declare auto delete option.
func WithExchangeDeclareAutoDelete(enable bool) ExchangeDeclareOption {
return func(o *exchangeDeclareOptions) {
o.autoDelete = enable
}
}
// WithExchangeDeclareInternal set exchange declare internal option.
func WithExchangeDeclareInternal(enable bool) ExchangeDeclareOption {
return func(o *exchangeDeclareOptions) {
o.internal = enable
}
}
// WithExchangeDeclareNoWait set exchange declare no wait option.
func WithExchangeDeclareNoWait(enable bool) ExchangeDeclareOption {
return func(o *exchangeDeclareOptions) {
o.noWait = enable
}
}
// WithExchangeDeclareArgs set exchange declare args option.
func WithExchangeDeclareArgs(args map[string]interface{}) ExchangeDeclareOption {
return func(o *exchangeDeclareOptions) {
o.args = args
}
}
// -------------------------------------------------------------------------------------------
// QueueBindOption declare queue bind option.
type QueueBindOption func(*queueBindOptions)
type queueBindOptions struct {
noWait bool // block processing
args amqp.Table // this parameter is invalid if the type is headers.
}
func (o *queueBindOptions) apply(opts ...QueueBindOption) {
for _, opt := range opts {
opt(o)
}
}
// default queue bind settings
func defaultQueueBindOptions() *queueBindOptions {
return &queueBindOptions{
noWait: false,
args: nil,
}
}
// WithQueueBindNoWait set queue bind no wait option.
func WithQueueBindNoWait(enable bool) QueueBindOption {
return func(o *queueBindOptions) {
o.noWait = enable
}
}
// WithQueueBindArgs set queue bind args option.
func WithQueueBindArgs(args map[string]interface{}) QueueBindOption {
return func(o *queueBindOptions) {
o.args = args
}
}
// -------------------------------------------------------------------------------------------
// DelayedMessagePublishOption declare queue bind option.
type DelayedMessagePublishOption func(*delayedMessagePublishOptions)
type delayedMessagePublishOptions struct {
topicKey string // the topic message type must be required
headersKeys map[string]interface{} // the headers message type must be required
}
func (o *delayedMessagePublishOptions) apply(opts ...DelayedMessagePublishOption) {
for _, opt := range opts {
opt(o)
}
}
// default delayed message publish settings
func defaultDelayedMessagePublishOptions() *delayedMessagePublishOptions {
return &delayedMessagePublishOptions{}
}
// WithDelayedMessagePublishTopicKey set delayed message publish topicKey option.
func WithDelayedMessagePublishTopicKey(topicKey string) DelayedMessagePublishOption {
return func(o *delayedMessagePublishOptions) {
if topicKey == "" {
return
}
o.topicKey = topicKey
}
}
// WithDelayedMessagePublishHeadersKeys set delayed message publish headersKeys option.
func WithDelayedMessagePublishHeadersKeys(headersKeys map[string]interface{}) DelayedMessagePublishOption {
return func(o *delayedMessagePublishOptions) {
if headersKeys == nil {
return
}
o.headersKeys = headersKeys
}
}
// -------------------------------------------------------------------------------------------
// DeadLetterOption declare dead letter option.
type DeadLetterOption func(*deadLetterOptions)
type deadLetterOptions struct {
exchangeName string
queueName string
routingKey string
exchangeDeclare *exchangeDeclareOptions
queueDeclare *queueDeclareOptions
queueBind *queueBindOptions
}
func (o *deadLetterOptions) apply(opts ...DeadLetterOption) {
for _, opt := range opts {
opt(o)
}
}
func (o *deadLetterOptions) isEnabled() bool {
if o.exchangeName != "" && o.queueName != "" {
return true
}
return false
}
func defaultDeadLetterOptions() *deadLetterOptions {
return &deadLetterOptions{
exchangeDeclare: defaultExchangeDeclareOptions(),
queueDeclare: defaultQueueDeclareOptions(),
queueBind: defaultQueueBindOptions(),
}
}
// WithDeadLetterExchangeDeclareOptions set dead letter exchange declare option.
func WithDeadLetterExchangeDeclareOptions(opts ...ExchangeDeclareOption) DeadLetterOption {
return func(o *deadLetterOptions) {
o.exchangeDeclare.apply(opts...)
}
}
// WithDeadLetterQueueDeclareOptions set dead letter queue declare option.
func WithDeadLetterQueueDeclareOptions(opts ...QueueDeclareOption) DeadLetterOption {
return func(o *deadLetterOptions) {
o.queueDeclare.apply(opts...)
}
}
// WithDeadLetterQueueBindOptions set dead letter queue bind option.
func WithDeadLetterQueueBindOptions(opts ...QueueBindOption) DeadLetterOption {
return func(o *deadLetterOptions) {
o.queueBind.apply(opts...)
}
}
// WithDeadLetter set dead letter exchange, queue, routing key.
func WithDeadLetter(exchangeName string, queueName string, routingKey string) DeadLetterOption {
return func(o *deadLetterOptions) {
o.exchangeName = exchangeName
o.queueName = queueName
o.routingKey = routingKey
}
}
package rabbitmq
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExchange(t *testing.T) {
e := NewDirectExchange("foo", "bar")
assert.Equal(t, e.eType, exchangeTypeDirect)
e = NewTopicExchange("foo", "bar.*")
assert.Equal(t, e.eType, exchangeTypeTopic)
e = NewFanoutExchange("foo")
assert.Equal(t, e.eType, exchangeTypeFanout)
e = NewHeadersExchange("foo", HeadersTypeAll, nil)
assert.Equal(t, e.eType, exchangeTypeHeaders)
e = NewHeadersExchange("foo", "unknown", nil)
assert.Equal(t, e.eType, exchangeTypeHeaders)
e = NewDelayedMessageExchange("foobar", NewDirectExchange("", "key"))
assert.Equal(t, e.eType, exchangeTypeDelayedMessage)
e = NewDelayedMessageExchange("foobar", NewDirectExchange("", "key"))
assert.Equal(t, e.name, e.Name())
assert.Equal(t, e.eType, e.Type())
assert.Equal(t, e.routingKey, e.RoutingKey())
assert.Equal(t, e.delayedMessageType, e.DelayedMessageType())
assert.Equal(t, e.headersKeys, e.HeadersKeys())
}
func TestExchangeDeclareOptions(t *testing.T) {
opts := []ExchangeDeclareOption{
WithExchangeDeclareAutoDelete(true),
WithExchangeDeclareInternal(true),
WithExchangeDeclareNoWait(true),
WithExchangeDeclareArgs(map[string]interface{}{"foo": "bar"}),
}
o := defaultExchangeDeclareOptions()
o.apply(opts...)
assert.True(t, o.autoDelete)
assert.True(t, o.internal)
assert.True(t, o.noWait)
assert.Equal(t, "bar", o.args["foo"])
}
func TestQueueDeclareOptions(t *testing.T) {
opts := []QueueDeclareOption{
WithQueueDeclareAutoDelete(true),
WithQueueDeclareExclusive(true),
WithQueueDeclareNoWait(true),
WithQueueDeclareArgs(map[string]interface{}{"foo": "bar"}),
}
o := defaultQueueDeclareOptions()
o.apply(opts...)
assert.True(t, o.autoDelete)
assert.True(t, o.exclusive)
assert.True(t, o.noWait)
assert.Equal(t, "bar", o.args["foo"])
}
func TestQueueBindOptions(t *testing.T) {
opts := []QueueBindOption{
WithQueueBindNoWait(true),
WithQueueBindArgs(map[string]interface{}{"foo": "bar"}),
}
o := defaultQueueBindOptions()
o.apply(opts...)
assert.True(t, o.noWait)
assert.Equal(t, "bar", o.args["foo"])
}
func TestDelayedMessagePublishOptions(t *testing.T) {
opts := []DelayedMessagePublishOption{
WithDelayedMessagePublishTopicKey(""),
WithDelayedMessagePublishTopicKey("key1.key2"),
WithDelayedMessagePublishHeadersKeys(nil),
WithDelayedMessagePublishHeadersKeys(map[string]interface{}{"foo": "bar"}),
}
o := defaultDelayedMessagePublishOptions()
o.apply(opts...)
assert.Equal(t, "key1.key2", o.topicKey)
assert.Equal(t, "bar", o.headersKeys["foo"])
}
func TestDelayedMessageConsumeOptions(t *testing.T) {
opts := []DeadLetterOption{
WithDeadLetter("dl-exchange", "dl-queue", "dl-routing-key"),
WithDeadLetterExchangeDeclareOptions(WithExchangeDeclareAutoDelete(false)),
WithDeadLetterQueueDeclareOptions(WithQueueDeclareAutoDelete(false)),
WithDeadLetterQueueBindOptions(WithQueueBindArgs(map[string]interface{}{"foo": "bar"})),
}
o := defaultDeadLetterOptions()
o.apply(opts...)
assert.Equal(t, "dl-exchange", o.exchangeName)
assert.Equal(t, "dl-queue", o.queueName)
assert.Equal(t, "dl-routing-key", o.routingKey)
assert.Equal(t, true, o.isEnabled())
o = defaultDeadLetterOptions()
o.apply()
assert.Equal(t, false, o.isEnabled())
}
// Package rabbitmq is a go wrapper for github.com/rabbitmq/amqp091-go
//
// producer and consumer using the five types direct, topic, fanout, headers, x-delayed-message.
// publisher and subscriber using the fanout message type.
package rabbitmq
import (
"crypto/tls"
"errors"
"fmt"
"strings"
"sync"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"go.uber.org/zap"
)
// DefaultURL default rabbitmq url
const DefaultURL = "amqp://guest:guest@localhost:5672/"
var defaultLogger, _ = zap.NewProduction()
// ConnectionOption connection option.
type ConnectionOption func(*connectionOptions)
type connectionOptions struct {
tlsConfig *tls.Config // tls config, if the url is amqps this field must be set
reconnectTime time.Duration // reconnect time interval, default is 3s
zapLog *zap.Logger
}
func (o *connectionOptions) apply(opts ...ConnectionOption) {
for _, opt := range opts {
opt(o)
}
}
// default connection settings
func defaultConnectionOptions() *connectionOptions {
return &connectionOptions{
tlsConfig: nil,
reconnectTime: time.Second * 3,
zapLog: defaultLogger,
}
}
// WithTLSConfig set tls config option.
func WithTLSConfig(tlsConfig *tls.Config) ConnectionOption {
return func(o *connectionOptions) {
if tlsConfig == nil {
tlsConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
o.tlsConfig = tlsConfig
}
}
// WithReconnectTime set reconnect time interval option.
func WithReconnectTime(d time.Duration) ConnectionOption {
return func(o *connectionOptions) {
if d == 0 {
d = time.Second * 3
}
o.reconnectTime = d
}
}
// WithLogger set logger option.
func WithLogger(zapLog *zap.Logger) ConnectionOption {
return func(o *connectionOptions) {
if zapLog == nil {
return
}
o.zapLog = zapLog
}
}
// -------------------------------------------------------------------------------------------
// Connection rabbitmq connection
type Connection struct {
mutex sync.Mutex
url string
tlsConfig *tls.Config
reconnectTime time.Duration
exit chan struct{}
zapLog *zap.Logger
conn *amqp.Connection
blockChan chan amqp.Blocking
closeChan chan *amqp.Error
isConnected bool
}
// NewConnection rabbitmq connection
func NewConnection(url string, opts ...ConnectionOption) (*Connection, error) {
if url == "" {
return nil, errors.New("url is empty")
}
o := defaultConnectionOptions()
o.apply(opts...)
connection := &Connection{
url: url,
reconnectTime: o.reconnectTime,
tlsConfig: o.tlsConfig,
exit: make(chan struct{}),
zapLog: o.zapLog,
}
conn, err := connect(connection.url, connection.tlsConfig)
if err != nil {
return nil, err
}
connection.zapLog.Info("[rabbitmq connection] connected successfully.")
connection.conn = conn
connection.blockChan = connection.conn.NotifyBlocked(make(chan amqp.Blocking, 1))
connection.closeChan = connection.conn.NotifyClose(make(chan *amqp.Error, 1))
connection.isConnected = true
go connection.monitor()
return connection, nil
}
func connect(url string, tlsConfig *tls.Config) (*amqp.Connection, error) {
var (
conn *amqp.Connection
err error
)
if strings.HasPrefix(url, "amqps://") {
if tlsConfig == nil {
return nil, errors.New("tls not set, e.g. NewConnection(url, WithTLSConfig(tlsConfig))")
}
conn, err = amqp.DialTLS(url, tlsConfig)
if err != nil {
return nil, err
}
} else {
conn, err = amqp.Dial(url)
if err != nil {
return nil, err
}
}
return conn, nil
}
// CheckConnected rabbitmq connection
func (c *Connection) CheckConnected() bool {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.isConnected
}
func (c *Connection) monitor() {
retryCount := 0
reconnectTip := fmt.Sprintf("[rabbitmq connection] lost connection, attempting reconnect in %s", c.reconnectTime)
for {
select {
case <-c.exit:
_ = c.closeConn()
c.zapLog.Info("[rabbitmq connection] closed")
return
case b := <-c.blockChan:
if b.Active {
c.zapLog.Warn("[rabbitmq connection] TCP blocked: " + b.Reason)
} else {
c.zapLog.Warn("[rabbitmq connection] TCP unblocked")
}
case <-c.closeChan:
c.mutex.Lock()
c.isConnected = false
c.mutex.Unlock()
retryCount++
c.zapLog.Warn(reconnectTip)
time.Sleep(c.reconnectTime) // wait for reconnect
amqpConn, amqpErr := connect(c.url, c.tlsConfig)
if amqpErr != nil {
c.zapLog.Warn("[rabbitmq connection] reconnect failed", zap.String("err", amqpErr.Error()), zap.Int("retryCount", retryCount))
continue
}
c.zapLog.Info("[rabbitmq connection] reconnected successfully.")
// set new connection
c.mutex.Lock()
c.isConnected = true
c.conn = amqpConn
c.blockChan = c.conn.NotifyBlocked(make(chan amqp.Blocking, 1))
c.closeChan = c.conn.NotifyClose(make(chan *amqp.Error, 1))
c.mutex.Unlock()
}
}
}
// Close rabbitmq connection
func (c *Connection) Close() {
c.mutex.Lock()
c.isConnected = false
c.mutex.Unlock()
close(c.exit)
}
func (c *Connection) closeConn() error {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.conn != nil {
return c.conn.Close()
}
return nil
}
package rabbitmq
import (
"context"
"crypto/tls"
"testing"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
var (
url = "amqp://guest:guest@192.168.3.37:5672/"
urlTLS = "amqps://guest:guest@127.0.0.1:5672/"
datetimeLayout = "2006-01-02 15:04:05.000"
)
func TestConnectionOptions(t *testing.T) {
opts := []ConnectionOption{
WithLogger(nil),
WithLogger(zap.NewNop()),
WithReconnectTime(time.Second),
WithTLSConfig(nil),
WithTLSConfig(&tls.Config{
InsecureSkipVerify: true,
}),
}
o := defaultConnectionOptions()
o.apply(opts...)
}
func TestNewConnection1(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*2, func(cancel context.CancelFunc) {
defer cancel()
c, err := NewConnection("")
assert.Error(t, err)
c, err = NewConnection(url)
if err != nil {
t.Log(err)
return
}
assert.True(t, c.CheckConnected())
time.Sleep(time.Second)
c.Close()
})
}
func TestNewConnection2(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*2, func(cancel context.CancelFunc) {
defer cancel()
// error
_, err := NewConnection(urlTLS)
assert.Error(t, err)
_, err = NewConnection(urlTLS, WithTLSConfig(&tls.Config{
InsecureSkipVerify: true,
}))
assert.Error(t, err)
})
}
func TestConnection_monitor(t *testing.T) {
c := &Connection{
url: urlTLS,
reconnectTime: time.Second,
exit: make(chan struct{}),
zapLog: defaultLogger,
conn: &amqp.Connection{},
blockChan: make(chan amqp.Blocking, 1),
closeChan: make(chan *amqp.Error, 1),
isConnected: true,
}
c.CheckConnected()
go func() {
defer func() { recover() }()
c.monitor()
}()
time.Sleep(time.Millisecond * 500)
c.mutex.Lock()
c.blockChan <- amqp.Blocking{Active: false}
c.blockChan <- amqp.Blocking{Active: true, Reason: "the disk is full."}
c.mutex.Unlock()
time.Sleep(time.Millisecond * 500)
c.mutex.Lock()
c.closeChan <- &amqp.Error{Code: 504, Reason: "connect failed"}
c.mutex.Unlock()
time.Sleep(time.Millisecond * 500)
c.Close()
time.Sleep(time.Millisecond * 500)
}
package rabbitmq
import (
"context"
"strconv"
"strings"
"sync/atomic"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"go.uber.org/zap"
)
// ConsumerOption consumer option.
type ConsumerOption func(*consumerOptions)
type consumerOptions struct {
exchangeDeclare *exchangeDeclareOptions
queueDeclare *queueDeclareOptions
queueBind *queueBindOptions
qos *qosOptions
consume *consumeOptions
isPersistent bool // persistent or not
isAutoAck bool // auto-answer or not, if false, manual ACK required
}
func (o *consumerOptions) apply(opts ...ConsumerOption) {
for _, opt := range opts {
opt(o)
}
}
// default consumer settings
func defaultConsumerOptions() *consumerOptions {
return &consumerOptions{
exchangeDeclare: defaultExchangeDeclareOptions(),
queueDeclare: defaultQueueDeclareOptions(),
queueBind: defaultQueueBindOptions(),
qos: defaultQosOptions(),
consume: defaultConsumeOptions(),
isPersistent: true,
isAutoAck: true,
}
}
// WithConsumerExchangeDeclareOptions set exchange declare option.
func WithConsumerExchangeDeclareOptions(opts ...ExchangeDeclareOption) ConsumerOption {
return func(o *consumerOptions) {
o.exchangeDeclare.apply(opts...)
}
}
// WithConsumerQueueDeclareOptions set queue declare option.
func WithConsumerQueueDeclareOptions(opts ...QueueDeclareOption) ConsumerOption {
return func(o *consumerOptions) {
o.queueDeclare.apply(opts...)
}
}
// WithConsumerQueueBindOptions set queue bind option.
func WithConsumerQueueBindOptions(opts ...QueueBindOption) ConsumerOption {
return func(o *consumerOptions) {
o.queueBind.apply(opts...)
}
}
// WithConsumerQosOptions set consume qos option.
func WithConsumerQosOptions(opts ...QosOption) ConsumerOption {
return func(o *consumerOptions) {
o.qos.apply(opts...)
}
}
// WithConsumerConsumeOptions set consumer consume option.
func WithConsumerConsumeOptions(opts ...ConsumeOption) ConsumerOption {
return func(o *consumerOptions) {
o.consume.apply(opts...)
}
}
// WithConsumerAutoAck set consumer auto ack option, if false, manual ACK required.
func WithConsumerAutoAck(enable bool) ConsumerOption {
return func(o *consumerOptions) {
o.isAutoAck = enable
}
}
// WithConsumerPersistent set consumer persistent option.
func WithConsumerPersistent(enable bool) ConsumerOption {
return func(o *consumerOptions) {
o.isPersistent = enable
}
}
// -------------------------------------------------------------------------------------------
// ConsumeOption consume option.
type ConsumeOption func(*consumeOptions)
type consumeOptions struct {
consumer string // used to distinguish between multiple consumers
exclusive bool // only available to the program that created it
noLocal bool // if set to true, a message sent by a producer in the same Connection cannot be passed to a consumer in this Connection.
noWait bool // block processing
args amqp.Table // additional properties
}
func (o *consumeOptions) apply(opts ...ConsumeOption) {
for _, opt := range opts {
opt(o)
}
}
// default consume settings
func defaultConsumeOptions() *consumeOptions {
return &consumeOptions{
consumer: "",
exclusive: false,
noLocal: false,
noWait: false,
args: nil,
}
}
// WithConsumeConsumer set consume consumer option.
func WithConsumeConsumer(consumer string) ConsumeOption {
return func(o *consumeOptions) {
o.consumer = consumer
}
}
// WithConsumeExclusive set consume exclusive option.
func WithConsumeExclusive(enable bool) ConsumeOption {
return func(o *consumeOptions) {
o.exclusive = enable
}
}
// WithConsumeNoLocal set consume noLocal option.
func WithConsumeNoLocal(enable bool) ConsumeOption {
return func(o *consumeOptions) {
o.noLocal = enable
}
}
// WithConsumeNoWait set consume no wait option.
func WithConsumeNoWait(enable bool) ConsumeOption {
return func(o *consumeOptions) {
o.noWait = enable
}
}
// WithConsumeArgs set consume args option.
func WithConsumeArgs(args map[string]interface{}) ConsumeOption {
return func(o *consumeOptions) {
o.args = args
}
}
// -------------------------------------------------------------------------------------------
// QosOption qos option.
type QosOption func(*qosOptions)
type qosOptions struct {
enable bool
prefetchCount int
prefetchSize int
global bool
}
func (o *qosOptions) apply(opts ...QosOption) {
for _, opt := range opts {
opt(o)
}
}
// default qos settings
func defaultQosOptions() *qosOptions {
return &qosOptions{
enable: false,
prefetchCount: 0,
prefetchSize: 0,
global: false,
}
}
// WithQosEnable set qos enable option.
func WithQosEnable() QosOption {
return func(o *qosOptions) {
o.enable = true
}
}
// WithQosPrefetchCount set qos prefetch count option.
func WithQosPrefetchCount(count int) QosOption {
return func(o *qosOptions) {
o.prefetchCount = count
}
}
// WithQosPrefetchSize set qos prefetch size option.
func WithQosPrefetchSize(size int) QosOption {
return func(o *qosOptions) {
o.prefetchSize = size
}
}
// WithQosPrefetchGlobal set qos global option.
func WithQosPrefetchGlobal(enable bool) QosOption {
return func(o *qosOptions) {
o.global = enable
}
}
// -------------------------------------------------------------------------------------------
// Consumer session
type Consumer struct {
Exchange *Exchange
QueueName string
connection *Connection
ch *amqp.Channel
exchangeDeclareOption *exchangeDeclareOptions
queueDeclareOption *queueDeclareOptions
queueBindOption *queueBindOptions
qosOption *qosOptions
consumeOption *consumeOptions
isPersistent bool // persistent or not
isAutoAck bool // auto ack or not
zapLog *zap.Logger
count int64 // consumer success message number
}
// Handler message
type Handler func(ctx context.Context, data []byte, tagID string) error
//type Handler func(ctx context.Context, d *amqp.Delivery, isAutoAck bool) error
// NewConsumer create a consumer
func NewConsumer(exchange *Exchange, queueName string, connection *Connection, opts ...ConsumerOption) (*Consumer, error) {
o := defaultConsumerOptions()
o.apply(opts...)
c := &Consumer{
Exchange: exchange,
QueueName: queueName,
connection: connection,
exchangeDeclareOption: o.exchangeDeclare,
queueDeclareOption: o.queueDeclare,
queueBindOption: o.queueBind,
qosOption: o.qos,
consumeOption: o.consume,
isPersistent: o.isPersistent,
isAutoAck: o.isAutoAck,
zapLog: connection.zapLog,
}
return c, nil
}
// initialize a consumer session
func (c *Consumer) initialize() error {
c.connection.mutex.Lock()
// crate a new channel
ch, err := c.connection.conn.Channel()
if err != nil {
c.connection.mutex.Unlock()
return err
}
c.ch = ch
c.connection.mutex.Unlock()
if c.Exchange.eType == exchangeTypeDelayedMessage {
if c.exchangeDeclareOption.args == nil {
c.exchangeDeclareOption.args = amqp.Table{
"x-delayed-type": c.Exchange.delayedMessageType,
}
} else {
c.exchangeDeclareOption.args["x-delayed-type"] = c.Exchange.delayedMessageType
}
}
// declare the exchange type
err = ch.ExchangeDeclare(
c.Exchange.name,
c.Exchange.eType,
c.isPersistent,
c.exchangeDeclareOption.autoDelete,
c.exchangeDeclareOption.internal,
c.exchangeDeclareOption.noWait,
c.exchangeDeclareOption.args,
)
if err != nil {
_ = ch.Close()
return err
}
// declare a queue and create it automatically if it doesn't exist, or skip creation if it does.
queue, err := ch.QueueDeclare(
c.QueueName,
c.isPersistent,
c.queueDeclareOption.autoDelete,
c.queueDeclareOption.exclusive,
c.queueDeclareOption.noWait,
c.queueDeclareOption.args,
)
if err != nil {
_ = ch.Close()
return err
}
args := c.queueBindOption.args
if c.Exchange.eType == exchangeTypeHeaders {
args = c.Exchange.headersKeys
}
// binding queue and exchange
err = ch.QueueBind(
queue.Name,
c.Exchange.routingKey,
c.Exchange.name,
c.queueBindOption.noWait,
args,
)
if err != nil {
_ = ch.Close()
return err
}
// setting the prefetch value, set channel.Qos on the consumer side to limit the number of messages consumed at a time,
// balancing message throughput and fairness, and prevent consumers from being hit by sudden bursts of information traffic.
if c.qosOption.enable {
err = ch.Qos(c.qosOption.prefetchCount, c.qosOption.prefetchSize, c.qosOption.global)
if err != nil {
_ = ch.Close()
return err
}
}
fields := logFields(c.QueueName, c.Exchange)
fields = append(fields, zap.Bool("autoAck", c.isAutoAck))
c.zapLog.Info("[rabbitmq consumer] initialized", fields...)
return nil
}
func (c *Consumer) consumeWithContext(ctx context.Context) (<-chan amqp.Delivery, error) {
return c.ch.ConsumeWithContext(
ctx,
c.QueueName,
c.consumeOption.consumer,
c.isAutoAck,
c.consumeOption.exclusive,
c.consumeOption.noLocal,
c.consumeOption.noWait,
c.consumeOption.args,
)
}
// Consume messages for loop in goroutine
func (c *Consumer) Consume(ctx context.Context, handler Handler) {
go func() {
ticker := time.NewTicker(time.Second * 2)
isFirst := true
for {
if isFirst {
isFirst = false
ticker.Reset(time.Millisecond * 10)
} else {
ticker.Reset(time.Second * 2)
}
// check connection for loop
select {
case <-ticker.C:
if !c.connection.CheckConnected() {
continue
}
case <-c.connection.exit:
c.Close()
return
}
ticker.Stop()
err := c.initialize()
if err != nil {
c.zapLog.Warn("[rabbitmq consumer] initialize consumer error", zap.String("err", err.Error()), zap.String("queue", c.QueueName))
continue
}
delivery, err := c.consumeWithContext(ctx)
if err != nil {
c.zapLog.Warn("[rabbitmq consumer] execution of consumption error", zap.String("err", err.Error()), zap.String("queue", c.QueueName))
continue
}
c.zapLog.Info("[rabbitmq consumer] queue is ready and waiting for messages, queue=" + c.QueueName)
isContinueConsume := false
for {
select {
case <-c.connection.exit:
c.Close()
return
case d, ok := <-delivery:
if !ok {
c.zapLog.Warn("[rabbitmq consumer] exit consume message, queue=" + c.QueueName)
isContinueConsume = true
break
}
tagID := strings.Join([]string{d.Exchange, c.QueueName, strconv.FormatUint(d.DeliveryTag, 10)}, "/")
err = handler(ctx, d.Body, tagID)
if err != nil {
c.zapLog.Warn("[rabbitmq consumer] handle message error", zap.String("err", err.Error()), zap.String("tagID", tagID))
continue
}
if !c.isAutoAck {
if err = d.Ack(false); err != nil {
c.zapLog.Warn("[rabbitmq consumer] manual ack error", zap.String("err", err.Error()), zap.String("tagID", tagID))
continue
}
c.zapLog.Info("[rabbitmq consumer] manual ack done", zap.String("tagID", tagID))
}
atomic.AddInt64(&c.count, 1)
}
if isContinueConsume {
break
}
}
}
}()
}
// Close consumer
func (c *Consumer) Close() {
if c.ch != nil {
_ = c.ch.Close()
}
}
// Count consumer success message number
func (c *Consumer) Count() int64 {
return atomic.LoadInt64(&c.count)
}
package rabbitmq
import (
"context"
"fmt"
"testing"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func TestConsumerOptions(t *testing.T) {
opts := []ConsumerOption{
WithConsumerExchangeDeclareOptions(
WithExchangeDeclareAutoDelete(true),
WithExchangeDeclareInternal(true),
WithExchangeDeclareNoWait(true),
WithExchangeDeclareArgs(map[string]interface{}{"foo1": "bar1"}),
),
WithConsumerQueueDeclareOptions(
WithQueueDeclareAutoDelete(true),
WithQueueDeclareExclusive(true),
WithQueueDeclareNoWait(true),
WithQueueDeclareArgs(map[string]interface{}{"foo": "bar"}),
),
WithConsumerQueueBindOptions(
WithQueueBindNoWait(true),
WithQueueBindArgs(map[string]interface{}{"foo2": "bar2"}),
),
WithConsumerConsumeOptions(
WithConsumeConsumer("test"),
WithConsumeExclusive(true),
WithConsumeNoLocal(true),
WithConsumeNoWait(true),
WithConsumeArgs(map[string]interface{}{"foo": "bar"}),
),
WithConsumerQosOptions(
WithQosEnable(),
WithQosPrefetchCount(1),
WithQosPrefetchSize(4096),
WithQosPrefetchGlobal(true),
),
WithConsumerAutoAck(true),
WithConsumerPersistent(true),
}
o := defaultConsumerOptions()
o.apply(opts...)
assert.True(t, o.queueDeclare.autoDelete)
assert.True(t, o.queueDeclare.exclusive)
assert.True(t, o.queueDeclare.noWait)
assert.Equal(t, "bar", o.queueDeclare.args["foo"])
assert.True(t, o.exchangeDeclare.autoDelete)
assert.True(t, o.exchangeDeclare.internal)
assert.True(t, o.exchangeDeclare.noWait)
assert.Equal(t, "bar1", o.exchangeDeclare.args["foo1"])
assert.True(t, o.queueBind.noWait)
assert.Equal(t, "bar2", o.queueBind.args["foo2"])
assert.True(t, o.isPersistent)
assert.True(t, o.isAutoAck)
}
var handler = func(ctx context.Context, data []byte, tagID string) error {
fmt.Printf("[received]: tagID=%s, data=%s\n", tagID, data)
return nil
}
func consume(ctx context.Context, queueName string, exchange *Exchange) error {
var consumeErr error
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
consumeErr = err
return
}
c, err := NewConsumer(exchange, queueName, connection, WithConsumerAutoAck(false))
if err != nil {
consumeErr = err
return
}
c.Consume(ctx, handler)
})
return consumeErr
}
func TestConsumer_direct(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
exchangeName := "direct-exchange-demo"
queueName := "direct-queue-1"
routeKey := "direct-key-1"
exchange := NewDirectExchange(exchangeName, routeKey)
err := producerDirect(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
err = consume(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
<-ctx.Done()
time.Sleep(time.Millisecond * 100)
}
func TestConsumer_topic(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
exchangeName := "topic-exchange-demo"
queueName := "topic-queue-1"
routingKey := "key1.key2.*"
exchange := NewTopicExchange(exchangeName, routingKey)
err := producerTopic(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
err = consume(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
<-ctx.Done()
time.Sleep(time.Millisecond * 100)
}
func TestConsumer_fanout(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
exchangeName := "fanout-exchange-demo"
queueName := "fanout-queue-1"
exchange := NewFanoutExchange(exchangeName)
err := producerFanout(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
err = consume(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
<-ctx.Done()
time.Sleep(time.Millisecond * 100)
}
func TestConsumer_headers(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
exchangeName := "headers-exchange-demo"
queueName := "headers-queue-1"
kv1 := map[string]interface{}{"hello1": "world1", "foo1": "bar1"}
exchange := NewHeadersExchange(exchangeName, HeadersTypeAll, kv1) // all
err := producerHeaders(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
err = consume(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
<-ctx.Done()
time.Sleep(time.Millisecond * 100)
}
func TestConsumer_delayedMessage(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), time.Second*7)
exchangeName := "delayed-message-exchange-demo"
queueName := "delayed-message-queue"
routingKey := "delayed-key"
exchange := NewDelayedMessageExchange(exchangeName, NewDirectExchange("", routingKey))
err := producerDelayedMessage(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
err = consume(ctx, queueName, exchange)
if err != nil {
t.Log(err)
return
}
<-ctx.Done()
time.Sleep(time.Millisecond * 100)
}
func producerDirect(ctx context.Context, queueName string, exchange *Exchange) error {
var producerErr error
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
producerErr = err
return
}
defer connection.Close()
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
producerErr = err
return
}
defer p.Close()
_ = p.PublishDirect(ctx, []byte("say hello 1"))
_ = p.PublishDirect(ctx, []byte("say hello 2"))
producerErr = p.PublishDirect(ctx, []byte("say hello 3"))
})
return producerErr
}
func producerTopic(ctx context.Context, queueName string, exchange *Exchange) error {
var producerErr error
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
producerErr = err
return
}
defer connection.Close()
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
producerErr = err
return
}
defer p.Close()
key := "key1.key2.key3"
producerErr = p.PublishTopic(ctx, key, []byte(key+" say hello"))
})
return producerErr
}
func producerFanout(ctx context.Context, queueName string, exchange *Exchange) error {
var producerErr error
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
producerErr = err
return
}
defer connection.Close()
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
producerErr = err
return
}
defer p.Close()
producerErr = p.PublishFanout(ctx, []byte(" say hello"))
})
return producerErr
}
func producerHeaders(ctx context.Context, queueName string, exchange *Exchange) error {
var producerErr error
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
producerErr = err
return
}
defer connection.Close()
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
producerErr = err
return
}
defer p.Close()
headersKey1 := exchange.headersKeys
err = p.PublishHeaders(ctx, headersKey1, []byte("say hello 1"))
if err != nil {
producerErr = err
return
}
headersKey1 = map[string]interface{}{"foo": "bar"}
producerErr = p.PublishHeaders(ctx, headersKey1, []byte("say hello 2"))
})
return producerErr
}
func producerDelayedMessage(ctx context.Context, queueName string, exchange *Exchange) error {
var producerErr error
utils.SafeRunWithTimeout(time.Second*6, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
producerErr = err
return
}
defer connection.Close()
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
producerErr = err
return
}
defer p.Close()
producerErr = p.PublishDelayedMessage(ctx, time.Second*5, []byte("say hello "+time.Now().Format(datetimeLayout)))
time.Sleep(time.Second)
producerErr = p.PublishDelayedMessage(ctx, time.Second*5, []byte("say hello "+time.Now().Format(datetimeLayout)))
})
return producerErr
}
func TestConsumerErr(t *testing.T) {
connection := &Connection{
exit: make(chan struct{}),
zapLog: zap.NewNop(),
conn: &amqp.Connection{},
isConnected: true,
}
exchange := NewDirectExchange("foo", "bar")
c, err := NewConsumer(exchange, "test", connection, WithConsumerQosOptions(
WithQosEnable(),
WithQosPrefetchCount(1)),
)
if err != nil {
t.Log(err)
return
}
t.Log(c.Count())
c.ch = &amqp.Channel{}
utils.SafeRunWithTimeout(time.Second, func(cancel context.CancelFunc) {
defer cancel()
err := c.initialize()
if err != nil {
t.Log(err)
return
}
})
utils.SafeRunWithTimeout(time.Second, func(cancel context.CancelFunc) {
defer cancel()
_, err := c.consumeWithContext(context.Background())
if err != nil {
t.Log(err)
return
}
})
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
c.Consume(context.Background(), handler)
})
utils.SafeRun(context.Background(), func(ctx context.Context) {
c.Close()
})
time.Sleep(time.Millisecond * 2500)
close(c.connection.exit)
}
package rabbitmq
import (
"context"
"fmt"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"go.uber.org/zap"
)
// ProducerOption producer option.
type ProducerOption func(*producerOptions)
type producerOptions struct {
exchangeDeclare *exchangeDeclareOptions
queueDeclare *queueDeclareOptions
queueBind *queueBindOptions
deadLetter *deadLetterOptions
isPersistent bool // is it persistent
// If true, the message will be returned to the sender if the queue cannot be
// found according to its own exchange type and routeKey rules.
mandatory bool
// only for publish-subscribe mode
isPublisherConfirm bool
}
func (o *producerOptions) apply(opts ...ProducerOption) {
for _, opt := range opts {
opt(o)
}
}
// default producer settings
func defaultProducerOptions() *producerOptions {
return &producerOptions{
exchangeDeclare: defaultExchangeDeclareOptions(),
queueDeclare: defaultQueueDeclareOptions(),
queueBind: defaultQueueBindOptions(),
deadLetter: defaultDeadLetterOptions(),
isPersistent: true,
mandatory: true,
isPublisherConfirm: false,
}
}
// WithProducerExchangeDeclareOptions set exchange declare option.
func WithProducerExchangeDeclareOptions(opts ...ExchangeDeclareOption) ProducerOption {
return func(o *producerOptions) {
o.exchangeDeclare.apply(opts...)
}
}
// WithProducerQueueDeclareOptions set queue declare option.
func WithProducerQueueDeclareOptions(opts ...QueueDeclareOption) ProducerOption {
return func(o *producerOptions) {
o.queueDeclare.apply(opts...)
}
}
// WithProducerQueueBindOptions set queue bind option.
func WithProducerQueueBindOptions(opts ...QueueBindOption) ProducerOption {
return func(o *producerOptions) {
o.queueBind.apply(opts...)
}
}
// WithDeadLetterOptions set dead letter options.
func WithDeadLetterOptions(opts ...DeadLetterOption) ProducerOption {
return func(o *producerOptions) {
o.deadLetter.apply(opts...)
}
}
// WithProducerPersistent set producer persistent option.
func WithProducerPersistent(enable bool) ProducerOption {
return func(o *producerOptions) {
o.isPersistent = enable
}
}
// WithProducerMandatory set producer mandatory option.
func WithProducerMandatory(enable bool) ProducerOption {
return func(o *producerOptions) {
o.mandatory = enable
}
}
// WithPublisherConfirm enables publisher confirm.
func WithPublisherConfirm() ProducerOption {
return func(o *producerOptions) {
o.isPublisherConfirm = true
}
}
// -------------------------------------------------------------------------------------------
// Producer session
type Producer struct {
Exchange *Exchange // exchange
QueueName string // queue name
conn *amqp.Connection // rabbitmq connection
ch *amqp.Channel // rabbitmq channel
// persistent or not
isPersistent bool
deliveryMode uint8 // amqp.Persistent or amqp.Transient
// If true, the message will be returned to the sender if the queue cannot be
// found according to its own exchange type and routeKey rules.
mandatory bool
zapLog *zap.Logger
exchangeArgs amqp.Table
queueArgs amqp.Table
queueBindArgs amqp.Table
// only for publish-subscribe mode
isPublisherConfirm bool
}
// NewProducer create a producer
func NewProducer(exchange *Exchange, queueName string, connection *Connection, opts ...ProducerOption) (*Producer, error) {
o := defaultProducerOptions()
o.apply(opts...)
// crate a new channel
ch, err := connection.conn.Channel()
if err != nil {
return nil, err
}
if exchange.eType == exchangeTypeDelayedMessage {
if o.exchangeDeclare.args == nil {
o.exchangeDeclare.args = amqp.Table{
"x-delayed-type": exchange.delayedMessageType,
}
} else {
o.exchangeDeclare.args["x-delayed-type"] = exchange.delayedMessageType
}
}
// declare the exchange type
err = ch.ExchangeDeclare(
exchange.name,
exchange.eType,
o.isPersistent,
o.exchangeDeclare.autoDelete,
o.exchangeDeclare.internal,
o.exchangeDeclare.noWait,
o.exchangeDeclare.args,
)
if err != nil {
_ = ch.Close()
return nil, err
}
// declare a queue and create it automatically if it doesn't exist, or skip creation if it does.
if o.deadLetter.isEnabled() {
if o.queueDeclare.args == nil {
o.queueDeclare.args = amqp.Table{
"x-dead-letter-exchange": o.deadLetter.exchangeName,
"x-dead-letter-routing-key": o.deadLetter.routingKey,
}
} else {
o.queueDeclare.args["x-dead-letter-exchange"] = o.deadLetter.exchangeName
o.queueDeclare.args["x-dead-letter-routing-key"] = o.deadLetter.routingKey
}
}
q, err := ch.QueueDeclare(
queueName,
o.isPersistent,
o.queueDeclare.autoDelete,
o.queueDeclare.exclusive,
o.queueDeclare.noWait,
o.queueDeclare.args,
)
if err != nil {
_ = ch.Close()
return nil, err
}
args := o.queueBind.args
if exchange.eType == exchangeTypeHeaders {
args = exchange.headersKeys
}
// binding queue and exchange
err = ch.QueueBind(
q.Name,
exchange.routingKey,
exchange.name,
o.queueBind.noWait,
args,
)
if err != nil {
_ = ch.Close()
return nil, err
}
fields := logFields(q.Name, exchange)
fields = append(fields, zap.Bool("isPersistent", o.isPersistent))
// create dead letter exchange and queue if enabled
if o.deadLetter.isEnabled() {
err = createDeadLetter(ch, o.deadLetter)
if err != nil {
_ = ch.Close()
return nil, err
}
fields = append(fields, zap.Any("deadLetter", map[string]string{
"exchange": o.deadLetter.exchangeName,
"queue": o.deadLetter.queueName,
"routingKey": o.deadLetter.routingKey,
"type": exchangeTypeDirect,
}))
}
deliveryMode := amqp.Persistent
if !o.isPersistent {
deliveryMode = amqp.Transient
}
connection.zapLog.Info("[rabbit producer] initialized", fields...)
return &Producer{
QueueName: queueName,
conn: connection.conn,
ch: ch,
Exchange: exchange,
isPersistent: o.isPersistent,
deliveryMode: deliveryMode,
mandatory: o.mandatory,
zapLog: connection.zapLog,
exchangeArgs: o.exchangeDeclare.args,
queueArgs: o.queueDeclare.args,
queueBindArgs: o.queueBind.args,
}, nil
}
// PublishDirect send direct type message
func (p *Producer) PublishDirect(ctx context.Context, body []byte) error {
if p.Exchange.eType != exchangeTypeDirect {
return fmt.Errorf("invalid exchange type (%s), only supports direct type", p.Exchange.eType)
}
return p.ch.PublishWithContext(
ctx,
p.Exchange.name,
p.Exchange.routingKey,
p.mandatory,
false,
amqp.Publishing{
DeliveryMode: p.deliveryMode,
ContentType: "text/plain",
Body: body,
},
)
}
// PublishFanout send fanout type message
func (p *Producer) PublishFanout(ctx context.Context, body []byte) error {
if p.Exchange.eType != exchangeTypeFanout {
return fmt.Errorf("invalid exchange type (%s), only supports fanout type", p.Exchange.eType)
}
return p.ch.PublishWithContext(
ctx,
p.Exchange.name,
p.Exchange.routingKey,
p.mandatory,
false,
amqp.Publishing{
DeliveryMode: p.deliveryMode,
ContentType: "text/plain",
Body: body,
},
)
}
// PublishTopic send topic type message
func (p *Producer) PublishTopic(ctx context.Context, topicKey string, body []byte) error {
if p.Exchange.eType != exchangeTypeTopic {
return fmt.Errorf("invalid exchange type (%s), only supports topic type", p.Exchange.eType)
}
return p.ch.PublishWithContext(
ctx,
p.Exchange.name,
topicKey,
p.mandatory,
false,
amqp.Publishing{
DeliveryMode: p.deliveryMode,
ContentType: "text/plain",
Body: body,
},
)
}
// PublishHeaders send headers type message
func (p *Producer) PublishHeaders(ctx context.Context, headersKeys map[string]interface{}, body []byte) error {
if p.Exchange.eType != exchangeTypeHeaders {
return fmt.Errorf("invalid exchange type (%s), only supports headers type", p.Exchange.eType)
}
return p.ch.PublishWithContext(
ctx,
p.Exchange.name,
p.Exchange.routingKey,
p.mandatory,
false,
amqp.Publishing{
DeliveryMode: p.deliveryMode,
Headers: headersKeys,
ContentType: "text/plain",
Body: body,
},
)
}
// PublishDelayedMessage send delayed type message
func (p *Producer) PublishDelayedMessage(ctx context.Context, delayTime time.Duration, body []byte, opts ...DelayedMessagePublishOption) error {
if p.Exchange.eType != exchangeTypeDelayedMessage {
return fmt.Errorf("invalid exchange type (%s), only supports x-delayed-message type", p.Exchange.eType)
}
routingKey := p.Exchange.routingKey
headersKeys := make(map[string]interface{})
o := defaultDelayedMessagePublishOptions()
o.apply(opts...)
switch p.Exchange.delayedMessageType {
case exchangeTypeTopic:
if o.topicKey == "" {
return fmt.Errorf("topic key is required, please set topicKey in DelayedMessagePublishOption")
}
routingKey = o.topicKey
case exchangeTypeHeaders:
if o.headersKeys == nil {
return fmt.Errorf("headers keys is required, please set headersKeys in DelayedMessagePublishOption")
}
headersKeys = o.headersKeys
}
headersKeys["x-delay"] = int(delayTime / time.Millisecond) // delay time: milliseconds
return p.ch.PublishWithContext(
ctx,
p.Exchange.name,
routingKey,
p.mandatory,
false,
amqp.Publishing{
DeliveryMode: p.deliveryMode,
Headers: headersKeys,
ContentType: "text/plain",
Body: body,
},
)
}
// Close the consumer
func (p *Producer) Close() {
if p.ch != nil {
_ = p.ch.Close()
}
}
// ExchangeArgs returns the exchange declare args.
func (p *Producer) ExchangeArgs() amqp.Table {
return p.exchangeArgs
}
// QueueArgs returns the queue declare args.
func (p *Producer) QueueArgs() amqp.Table {
return p.queueArgs
}
// QueueBindArgs returns the queue bind args.
func (p *Producer) QueueBindArgs() amqp.Table {
return p.queueBindArgs
}
func logFields(queueName string, exchange *Exchange) []zap.Field {
fields := []zap.Field{
zap.String("queue", queueName),
zap.String("exchange", exchange.name),
zap.String("exchangeType", exchange.eType),
}
switch exchange.eType {
case exchangeTypeDirect, exchangeTypeTopic:
fields = append(fields, zap.String("routingKey", exchange.routingKey))
case exchangeTypeHeaders:
fields = append(fields, zap.Any("headersKeys", exchange.headersKeys))
case exchangeTypeDelayedMessage:
fields = append(fields, zap.String("delayedMessageType", exchange.delayedMessageType))
switch exchange.delayedMessageType {
case exchangeTypeDirect, exchangeTypeTopic:
fields = append(fields, zap.String("routingKey", exchange.routingKey))
case exchangeTypeHeaders:
fields = append(fields, zap.Any("headersKeys", exchange.headersKeys))
}
}
return fields
}
// -------------------------------------------------------------------------------------------
func createDeadLetter(ch *amqp.Channel, o *deadLetterOptions) error {
// declare the exchange type
err := ch.ExchangeDeclare(
o.exchangeName,
exchangeTypeDirect,
true,
o.exchangeDeclare.autoDelete,
o.exchangeDeclare.internal,
o.exchangeDeclare.noWait,
o.exchangeDeclare.args,
)
if err != nil {
return err
}
// declare a queue and create it automatically if it doesn't exist, or skip creation if it does.
q, err := ch.QueueDeclare(
o.queueName,
true,
o.queueDeclare.autoDelete,
o.queueDeclare.exclusive,
o.queueDeclare.noWait,
o.queueDeclare.args,
)
if err != nil {
return err
}
// binding queue and exchange
err = ch.QueueBind(
q.Name,
o.routingKey,
o.exchangeName,
o.queueBind.noWait,
o.queueBind.args,
)
return err
}
package rabbitmq
import (
"context"
"strconv"
"testing"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/stretchr/testify/assert"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func TestProducerOptions(t *testing.T) {
opts := []ProducerOption{
WithProducerExchangeDeclareOptions(
WithExchangeDeclareAutoDelete(true),
WithExchangeDeclareInternal(true),
WithExchangeDeclareNoWait(true),
WithExchangeDeclareArgs(map[string]interface{}{"foo1": "bar1"}),
),
WithProducerQueueDeclareOptions(
WithQueueDeclareAutoDelete(true),
WithQueueDeclareExclusive(true),
WithQueueDeclareNoWait(true),
WithQueueDeclareArgs(map[string]interface{}{"foo": "bar"}),
),
WithProducerQueueBindOptions(
WithQueueBindNoWait(true),
WithQueueBindArgs(map[string]interface{}{"foo2": "bar2"}),
),
WithProducerPersistent(true),
WithProducerMandatory(true),
WithDeadLetterOptions(WithDeadLetter("dl-exchange", "dl-queue", "dl-routing-key")),
}
o := defaultProducerOptions()
o.apply(opts...)
assert.True(t, o.queueDeclare.autoDelete)
assert.True(t, o.queueDeclare.exclusive)
assert.True(t, o.queueDeclare.noWait)
assert.Equal(t, "bar", o.queueDeclare.args["foo"])
assert.True(t, o.exchangeDeclare.autoDelete)
assert.True(t, o.exchangeDeclare.internal)
assert.True(t, o.exchangeDeclare.noWait)
assert.Equal(t, "bar1", o.exchangeDeclare.args["foo1"])
assert.True(t, o.queueBind.noWait)
assert.Equal(t, "bar2", o.queueBind.args["foo2"])
assert.True(t, o.isPersistent)
assert.True(t, o.mandatory)
}
func TestProducer_direct(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
t.Log(err)
return
}
defer connection.Close()
ctx := context.Background()
exchangeName := "direct-exchange-demo"
queueName := "direct-queue-demo"
routingKey := "info"
exchange := NewDirectExchange(exchangeName, routingKey)
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
t.Log(err)
return
}
defer p.Close()
for i := 1; i <= 10; i++ {
err = p.PublishDirect(ctx, []byte(routingKey+" say hello "+strconv.Itoa(i)))
if err != nil {
t.Error(err)
return
}
}
})
}
func TestProducer_topic(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
t.Log(err)
return
}
defer connection.Close()
ctx := context.Background()
exchangeName := "topic-exchange-demo"
queueName := "topic-queue-1"
routingKey := "*.xmall.*"
exchange := NewTopicExchange(exchangeName, routingKey)
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
t.Log(err)
return
}
defer p.Close()
key := "key1.xmall.key3"
err = p.PublishTopic(ctx, key, []byte(key+" say hello"))
queueName = "topic-queue-2"
routingKey = "*.*.rabbit"
exchange = NewTopicExchange(exchangeName, routingKey)
p, err = NewProducer(exchange, queueName, connection)
if err != nil {
t.Log(err)
return
}
defer p.Close()
key = "key1.key2.rabbit"
err = p.PublishTopic(ctx, key, []byte(key+" say hello"))
queueName = "topic-queue-2"
routingKey = "lazy.#"
exchange = NewTopicExchange(exchangeName, routingKey)
p, err = NewProducer(exchange, queueName, connection)
if err != nil {
t.Log(err)
return
}
defer p.Close()
key = "lazy.key2.key3"
err = p.PublishTopic(ctx, key, []byte(key+" say hello"))
})
}
func TestProducer_fanout(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
t.Log(err)
return
}
defer connection.Close()
ctx := context.Background()
exchangeName := "fanout-exchange-demo"
queueNames := []string{"fanout-queue-1", "fanout-queue-2", "fanout-queue-3"}
for _, queueName := range queueNames {
exchange := NewFanoutExchange(exchangeName)
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
t.Log(err)
return
}
defer p.Close()
err = p.PublishFanout(ctx, []byte(queueName+" say hello"))
if err != nil {
t.Error(err)
return
}
}
})
}
func TestProducer_headers(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
t.Log(err)
return
}
defer connection.Close()
ctx := context.Background()
exchangeName := "headers-exchange-demo"
// the message is only received if there is an exact match for headers
queueName := "headers-queue-1"
kv1 := map[string]interface{}{"hello1": "world1", "foo1": "bar1"}
exchange := NewHeadersExchange(exchangeName, HeadersTypeAll, kv1)
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
t.Log(err)
return
}
defer p.Close()
headersKey1 := kv1 // exact match, consumer queue can receive messages
err = p.PublishHeaders(ctx, headersKey1, []byte("say hello 1"))
if err != nil {
t.Error(err)
return
}
headersKey1 = map[string]interface{}{"foo": "bar"} // there is a complete mismatch and the consumer queue cannot receive the message
err = p.PublishHeaders(ctx, headersKey1, []byte("say hello 2"))
if err != nil {
t.Error(err)
return
}
headersKey1 = map[string]interface{}{"foo1": "bar1"} // partial match, consumer queue cannot receive message
err = p.PublishHeaders(ctx, headersKey1, []byte("say hello 3"))
if err != nil {
t.Error(err)
return
}
// only partial matches of headers are needed to receive the message
queueName = "headers-queue-2"
kv2 := map[string]interface{}{"hello2": "world2", "foo2": "bar2"}
exchange = NewHeadersExchange(exchangeName, HeadersTypeAny, kv2)
p, err = NewProducer(exchange, queueName, connection)
if err != nil {
t.Error(err)
return
}
defer p.Close()
headersKey2 := kv2 // exact match, consumer queue can receive messages
err = p.PublishHeaders(ctx, headersKey2, []byte("say hello 4"))
if err != nil {
t.Error(err)
return
}
headersKey2 = map[string]interface{}{"foo": "bar"} // there is a complete mismatch and the consumer queue cannot receive the message
err = p.PublishHeaders(ctx, headersKey2, []byte("say hello 5"))
if err != nil {
t.Error(err)
return
}
headersKey2 = map[string]interface{}{"foo2": "bar2"} // partial match, the consumer queue can receive the message
err = p.PublishHeaders(ctx, headersKey2, []byte("say hello 6"))
if err != nil {
t.Error(err)
return
}
})
}
func TestProducer_delayedMessage(t *testing.T) {
utils.SafeRunWithTimeout(time.Second*6, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
t.Log(err)
return
}
defer connection.Close()
ctx := context.Background()
exchangeName := "delayed-message-exchange-demo"
queueName := "delayed-message-queue"
routingKey := "delayed-key"
e := NewDirectExchange("", routingKey)
exchange := NewDelayedMessageExchange(exchangeName, e)
p, err := NewProducer(exchange, queueName, connection)
if err != nil {
t.Log(err)
return
}
defer p.Close()
for i := 0; i < 3; i++ {
err = p.PublishDelayedMessage(ctx, time.Second*10, []byte("say hello "+time.Now().Format(datetimeLayout)))
if err != nil {
t.Error(err)
return
}
time.Sleep(time.Second)
}
})
}
func TestPublishErr(t *testing.T) {
p := &Producer{
QueueName: "test",
Exchange: &Exchange{
name: "test",
eType: "unknown",
routingKey: "test",
},
}
ctx := context.Background()
err := p.PublishDirect(ctx, []byte("data"))
assert.Error(t, err)
err = p.PublishFanout(ctx, []byte("data"))
assert.Error(t, err)
err = p.PublishTopic(ctx, "", []byte("data"))
assert.Error(t, err)
err = p.PublishHeaders(ctx, nil, []byte("data"))
assert.Error(t, err)
err = p.PublishDelayedMessage(ctx, time.Second, []byte("data"))
assert.Error(t, err)
}
func TestPublishDirect(t *testing.T) {
p := &Producer{
QueueName: "foo",
conn: &amqp.Connection{},
ch: &amqp.Channel{},
isPersistent: true,
mandatory: true,
}
defer func() { recover() }()
ctx := context.Background()
p.Exchange = NewDirectExchange("foo", "bar")
_ = p.PublishDirect(ctx, []byte("data"))
}
func TestPublishTopic(t *testing.T) {
p := &Producer{
QueueName: "foo",
conn: &amqp.Connection{},
ch: &amqp.Channel{},
isPersistent: true,
mandatory: true,
}
defer func() { recover() }()
ctx := context.Background()
p.Exchange = NewDirectExchange("foo", "bar")
_ = p.PublishTopic(ctx, "foo", []byte("data"))
p.Exchange = NewTopicExchange("foo", "bar")
_ = p.PublishTopic(ctx, "foo", []byte("data"))
}
func TestPublishFanout(t *testing.T) {
p := &Producer{
QueueName: "foo",
conn: &amqp.Connection{},
ch: &amqp.Channel{},
isPersistent: true,
mandatory: true,
}
defer func() { recover() }()
ctx := context.Background()
p.Exchange = NewFanoutExchange("foo")
_ = p.PublishFanout(ctx, []byte("data"))
}
func TestPublishHeaders(t *testing.T) {
p := &Producer{
QueueName: "foo",
conn: &amqp.Connection{},
ch: &amqp.Channel{},
isPersistent: true,
mandatory: true,
}
defer func() { recover() }()
ctx := context.Background()
p.Exchange = NewDirectExchange("foo", "bar")
_ = p.PublishHeaders(ctx, nil, []byte("data"))
p.Exchange = NewHeadersExchange("foo", "bar", nil)
_ = p.PublishHeaders(ctx, nil, []byte("data"))
}
func TestPublishDelayedMessage(t *testing.T) {
p := &Producer{
QueueName: "foo",
conn: &amqp.Connection{},
ch: &amqp.Channel{},
isPersistent: true,
mandatory: true,
}
defer func() { recover() }()
ctx := context.Background()
p.Exchange = NewDelayedMessageExchange("foo", NewTopicExchange("", "bar"))
_ = p.PublishDelayedMessage(ctx, time.Second, []byte("data"))
p.Exchange = NewDelayedMessageExchange("foo", NewHeadersExchange("", HeadersTypeAll, nil))
_ = p.PublishDelayedMessage(ctx, time.Second, []byte("data"))
p.Exchange = NewDelayedMessageExchange("foo", NewDirectExchange("", "bar"))
_ = p.PublishDelayedMessage(ctx, time.Second, []byte("data"))
}
func TestProducerErr(t *testing.T) {
exchangeName := "direct-exchange-demo"
queueName := "direct-queue-1"
routeKey := "direct-key-1"
exchange := NewDirectExchange(exchangeName, routeKey)
utils.SafeRunWithTimeout(time.Second, func(cancel context.CancelFunc) {
defer cancel()
_, err := NewProducer(exchange, queueName, &Connection{conn: &amqp.Connection{}})
if err != nil {
t.Log(err)
return
}
})
p := &Producer{conn: &amqp.Connection{}, ch: &amqp.Channel{}}
utils.SafeRun(context.Background(), func(ctx context.Context) {
_ = p.PublishDirect(context.Background(), []byte("hello world"))
})
utils.SafeRun(context.Background(), func(ctx context.Context) {
p.Close()
})
}
func Test_printFields(t *testing.T) {
exchange := NewDirectExchange("foo", "bar")
fields := logFields("queue", exchange)
t.Log(fields)
exchange = NewHeadersExchange("foo", HeadersTypeAny, map[string]interface{}{"hello": "world"})
fields = logFields("queue", exchange)
t.Log(fields)
e := NewDirectExchange("", "bar")
exchange = NewDelayedMessageExchange("foo", e)
fields = logFields("queue", exchange)
t.Log(fields)
e = NewHeadersExchange("", HeadersTypeAny, map[string]interface{}{"hello": "world"})
exchange = NewDelayedMessageExchange("foo", e)
fields = logFields("queue", exchange)
t.Log(fields)
}
package rabbitmq
import (
"context"
"fmt"
amqp "github.com/rabbitmq/amqp091-go"
"go.uber.org/zap"
)
// Publisher session
type Publisher struct {
*Producer
}
// NewPublisher create a publisher, channelName is exchange name
func NewPublisher(channelName string, connection *Connection, opts ...ProducerOption) (*Publisher, error) {
o := defaultProducerOptions()
o.apply(opts...)
exchange := NewFanoutExchange(channelName)
// crate a new channel
ch, err := connection.conn.Channel()
if err != nil {
return nil, err
}
// enable publisher confirm
if o.isPublisherConfirm {
err = ch.Confirm(false)
if err != nil {
_ = ch.Close()
return nil, err
}
}
// declare the exchange type
err = ch.ExchangeDeclare(
channelName,
exchangeTypeFanout,
o.isPersistent,
o.exchangeDeclare.autoDelete,
o.exchangeDeclare.internal,
o.exchangeDeclare.noWait,
o.exchangeDeclare.args,
)
if err != nil {
_ = ch.Close()
return nil, err
}
deliveryMode := amqp.Persistent
if !o.isPersistent {
deliveryMode = amqp.Transient
}
connection.zapLog.Info("[rabbit producer] initialized", zap.String("channel", channelName), zap.Bool("isPersistent", o.isPersistent))
p := &Producer{
Exchange: exchange,
conn: connection.conn,
ch: ch,
isPersistent: o.isPersistent,
deliveryMode: deliveryMode,
mandatory: o.mandatory,
zapLog: connection.zapLog,
}
return &Publisher{p}, nil
}
func (p *Publisher) Publish(ctx context.Context, body []byte) error {
err := p.ch.PublishWithContext(
ctx,
p.Exchange.name,
p.Exchange.routingKey,
p.mandatory,
false,
amqp.Publishing{
DeliveryMode: p.deliveryMode,
ContentType: "text/plain",
Body: body,
},
)
if err != nil {
return err
}
if p.isPublisherConfirm {
// wait for publisher confirm
select {
case <-ctx.Done():
return ctx.Err()
case confirm := <-p.ch.NotifyPublish(make(chan amqp.Confirmation, 1)):
if !confirm.Ack {
return fmt.Errorf("publisher confirm failed, exchangeName: %s, routingKey: %s, deliveryTag: %d",
p.Exchange.name, p.Exchange.routingKey, confirm.DeliveryTag)
}
}
}
return nil
}
// Close publisher
func (p *Publisher) Close() {
if p.ch != nil {
_ = p.ch.Close()
}
}
package rabbitmq
import (
"context"
"fmt"
"testing"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
var testChannelName = "pub-sub"
func runPublisher(ctx context.Context, channelName string) error {
var publisherErr error
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
publisherErr = err
return
}
defer connection.Close()
p, err := NewPublisher(channelName, connection)
if err != nil {
publisherErr = err
return
}
defer p.Close()
data := []byte("hello world " + time.Now().Format(datetimeLayout))
err = p.Publish(ctx, data)
if err != nil {
publisherErr = err
return
}
fmt.Printf("[send]: %s\n", data)
})
return publisherErr
}
func TestPublisher(t *testing.T) {
err := runPublisher(context.Background(), testChannelName)
if err != nil {
t.Log(err)
return
}
}
func TestPublisherErr(t *testing.T) {
utils.SafeRunWithTimeout(time.Second, func(cancel context.CancelFunc) {
defer cancel()
_, err := NewPublisher(testChannelName, &Connection{conn: &amqp.Connection{}})
if err != nil {
t.Log(err)
return
}
})
p := &Publisher{&Producer{conn: &amqp.Connection{}, ch: &amqp.Channel{}}}
utils.SafeRun(context.Background(), func(ctx context.Context) {
_ = p.Publish(context.Background(), []byte("hello world"))
})
utils.SafeRun(context.Background(), func(ctx context.Context) {
p.Close()
})
}
package rabbitmq
import (
"context"
)
// Subscriber session
type Subscriber struct {
*Consumer
}
// NewSubscriber create a subscriber, channelName is exchange name, identifier is queue name
func NewSubscriber(channelName string, identifier string, connection *Connection, opts ...ConsumerOption) (*Subscriber, error) {
exchange := NewFanoutExchange(channelName)
queueName := identifier
c, err := NewConsumer(exchange, queueName, connection, opts...)
if err != nil {
return nil, err
}
return &Subscriber{c}, nil
}
// Subscribe and handle message
func (s *Subscriber) Subscribe(ctx context.Context, handler Handler) {
s.Consume(ctx, handler)
}
// Close subscriber
func (s *Subscriber) Close() {
if s.ch != nil {
_ = s.ch.Close()
}
}
package rabbitmq
import (
"context"
"testing"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func TestSubscriber(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
err := runPublisher(ctx, testChannelName)
if err != nil {
t.Log(err)
return
}
err = runSubscriber(ctx, testChannelName, "fanout-queue-1")
if err != nil {
t.Log(err)
return
}
err = runSubscriber(ctx, testChannelName, "fanout-queue-2")
if err != nil {
t.Log(err)
return
}
<-ctx.Done()
time.Sleep(time.Millisecond * 100)
}
func runSubscriber(ctx context.Context, channelName string, identifier string) error {
var subscriberErr error
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
defer cancel()
connection, err := NewConnection(url)
if err != nil {
subscriberErr = err
return
}
s, err := NewSubscriber(channelName, identifier, connection, WithConsumerAutoAck(false))
if err != nil {
subscriberErr = err
return
}
s.Subscribe(ctx, handler)
})
return subscriberErr
}
func TestSubscriberErr(t *testing.T) {
utils.SafeRunWithTimeout(time.Second, func(cancel context.CancelFunc) {
defer cancel()
_, err := NewSubscriber(testChannelName, "fanout-queue-1", &Connection{conn: &amqp.Connection{}})
if err != nil {
t.Log(err)
return
}
})
s := &Subscriber{&Consumer{connection: &Connection{conn: &amqp.Connection{}}, ch: &amqp.Channel{}}}
utils.SafeRun(context.Background(), func(ctx context.Context) {
s.Subscribe(context.Background(), handler)
})
utils.SafeRun(context.Background(), func(ctx context.Context) {
s.Close()
})
}
// Package replacer is a library of replacement file content, supports replacement of
// files in local directories and embedded directory files via embed.
package replacer
import (
"bytes"
"embed"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"gitlab.wanzhuangkj.com/tush/xpkg/gofile"
)
var _ Replacer = (*replacerInfo)(nil)
// Replacer interface
type Replacer interface {
SetReplacementFields(fields []Field)
SetSubDirsAndFiles(subDirs []string, subFiles ...string)
SetIgnoreSubDirs(dirs ...string)
SetIgnoreSubFiles(filenames ...string)
SetOutputDir(absDir string, name ...string) error
GetOutputDir() string
GetSourcePath() string
SaveFiles() error
ReadFile(filename string) ([]byte, error)
GetFiles() []string
SaveTemplateFiles(m map[string]interface{}, parentDir ...string) error
}
// replacerInfo replacer information
type replacerInfo struct {
path string // template directory or file
fs embed.FS // Template directory corresponding to binary objects
isActual bool // true: use os to manipulate files, false: use fs to manipulate files
files []string // list of template files
ignoreFiles []string // ignore the list of replaced files, e.g. ignore.txt or myDir/ignore.txt
ignoreDirs []string // ignore processed subdirectories
replacementFields []Field // characters to be replaced when converting from a template file to a new file
outPath string // the directory where the file is saved after replacement
}
// New create replacer with local directory
func New(path string) (Replacer, error) {
files, err := gofile.ListFiles(path)
if err != nil {
return nil, err
}
path, _ = filepath.Abs(path)
return &replacerInfo{
path: path,
isActual: true,
files: files,
replacementFields: []Field{},
}, nil
}
// NewFS create replacer with embed.FS
func NewFS(path string, fs embed.FS) (Replacer, error) {
files, err := listFiles(path, fs)
if err != nil {
return nil, err
}
return &replacerInfo{
path: path,
fs: fs,
isActual: false,
files: files,
replacementFields: []Field{},
}, nil
}
// Field replace field information
type Field struct {
Old string // old field
New string // new field
IsCaseSensitive bool // whether the first letter is case-sensitive
}
// SetReplacementFields set the replacement field, note: old characters should not be included in the relationship,
// if they exist, pay attention to the order of precedence when setting the Field
func (r *replacerInfo) SetReplacementFields(fields []Field) {
var newFields []Field
for _, field := range fields {
if field.IsCaseSensitive && isFirstAlphabet(field.Old) { // splitting the initial case field
if field.New == "" {
continue
}
newFields = append(newFields,
Field{ // convert the first letter to upper case
Old: strings.ToUpper(field.Old[:1]) + field.Old[1:],
New: strings.ToUpper(field.New[:1]) + field.New[1:],
},
Field{ // convert the first letter to lower case
Old: strings.ToLower(field.Old[:1]) + field.Old[1:],
New: strings.ToLower(field.New[:1]) + field.New[1:],
},
)
} else {
newFields = append(newFields, field)
}
}
r.replacementFields = newFields
}
// GetFiles get files
func (r *replacerInfo) GetFiles() []string {
return r.files
}
// SetSubDirsAndFiles set up processing of specified subdirectories, files in other directories are ignored
func (r *replacerInfo) SetSubDirsAndFiles(subDirs []string, subFiles ...string) {
subDirs = r.convertPathsDelimiter(subDirs...)
subFiles = r.convertPathsDelimiter(subFiles...)
var files []string
isExistFile := make(map[string]struct{}) // use map to avoid duplicate files
for _, file := range r.files {
for _, dir := range subDirs {
if isSubPath(file, dir) {
if _, ok := isExistFile[file]; ok {
continue
}
isExistFile[file] = struct{}{}
files = append(files, file)
}
}
for _, sf := range subFiles {
if isMatchFile(file, sf) {
if _, ok := isExistFile[file]; ok {
continue
}
isExistFile[file] = struct{}{}
files = append(files, file)
}
}
}
if len(files) == 0 {
return
}
r.files = files
}
// SetIgnoreSubFiles specify files to be ignored
func (r *replacerInfo) SetIgnoreSubFiles(filenames ...string) {
r.ignoreFiles = append(r.ignoreFiles, filenames...)
}
// SetIgnoreSubDirs specify subdirectories to be ignored
func (r *replacerInfo) SetIgnoreSubDirs(dirs ...string) {
dirs = r.convertPathsDelimiter(dirs...)
r.ignoreDirs = append(r.ignoreDirs, dirs...)
}
// SetOutputDir specify the output directory, preferably using absPath, if absPath is empty,
// the output directory is automatically generated in the current directory according to the name of the parameter
func (r *replacerInfo) SetOutputDir(absPath string, name ...string) error {
// output to the specified directory
if absPath != "" {
abs, err := filepath.Abs(absPath)
if err != nil {
return err
}
r.outPath = abs
return nil
}
// output to the current directory
subPath := strings.Join(name, "_")
pwd, err := os.Getwd()
if err != nil {
return err
}
r.outPath = pwd + gofile.GetPathDelimiter() + subPath + "_" + time.Now().Format("150405")
return nil
}
// GetOutputDir get output directory
func (r *replacerInfo) GetOutputDir() string {
return r.outPath
}
// GetSourcePath get source directory
func (r *replacerInfo) GetSourcePath() string {
return r.path
}
// ReadFile read file content
func (r *replacerInfo) ReadFile(filename string) ([]byte, error) {
filename = r.convertPathDelimiter(filename)
foundFile := []string{}
for _, file := range r.files {
if strings.Contains(file, filename) && gofile.GetFilename(file) == gofile.GetFilename(filename) {
foundFile = append(foundFile, file)
}
}
if len(foundFile) != 1 {
return nil, fmt.Errorf("total %d file named '%s', files=%+v", len(foundFile), filename, foundFile)
}
if r.isActual {
return os.ReadFile(foundFile[0])
}
return r.fs.ReadFile(foundFile[0])
}
// SaveFiles save file with setting
func (r *replacerInfo) SaveFiles() error {
if r.outPath == "" {
r.outPath = gofile.GetRunPath() + gofile.GetPathDelimiter() + "generate_" + time.Now().Format("150405")
}
var existFiles []string
var writeData = make(map[string][]byte)
for _, file := range r.files {
if r.isInIgnoreDir(file) || r.isIgnoreFile(file) {
continue
}
var data []byte
var err error
if r.isActual {
data, err = os.ReadFile(file) // read from local files
} else {
data, err = r.fs.ReadFile(file) // read from local embed.FS
}
if err != nil {
return err
}
// replace text content
for _, field := range r.replacementFields {
data = bytes.ReplaceAll(data, []byte(field.Old), []byte(field.New))
}
// get new file path
newFilePath := r.getNewFilePath(file)
dir, filename := filepath.Split(newFilePath)
// replace file names and directory names
for _, field := range r.replacementFields {
if strings.Contains(dir, field.Old) {
dir = strings.ReplaceAll(dir, field.Old, field.New)
}
if strings.Contains(filename, field.Old) {
filename = strings.ReplaceAll(filename, field.Old, field.New)
}
if newFilePath != dir+filename {
newFilePath = dir + filename
}
}
if gofile.IsExists(newFilePath) {
existFiles = append(existFiles, newFilePath)
}
writeData[newFilePath] = data
}
if len(existFiles) > 0 {
//nolint
return fmt.Errorf("existing files detected\n %s\nCode generation has been cancelled\n",
strings.Join(existFiles, "\n "))
}
for file, data := range writeData {
if isForbiddenFile(file, r.path) {
return fmt.Errorf("disable writing file(%s) to directory(%s), file size=%d", file, r.path, len(data))
}
}
for file, data := range writeData {
err := saveToNewFile(file, data)
if err != nil {
return err
}
}
return nil
}
// SaveTemplateFiles save file with setting
func (r *replacerInfo) SaveTemplateFiles(m map[string]interface{}, parentDir ...string) error {
refDir := ""
if len(parentDir) > 0 {
refDir = strings.Join(parentDir, gofile.GetPathDelimiter())
}
writeData := make(map[string][]byte, len(r.files))
for _, file := range r.files {
data, err := replaceTemplateData(file, m)
if err != nil {
return err
}
newFilePath := r.getNewFilePath2(file, refDir)
newFilePath = trimExt(newFilePath)
if gofile.IsExists(newFilePath) {
return fmt.Errorf("file %s already exists, cancel code generation", newFilePath)
}
newFilePath, err = replaceTemplateFilePath(newFilePath, m)
if err != nil {
return err
}
writeData[newFilePath] = data
}
for file, data := range writeData {
err := saveToNewFile(file, data)
if err != nil {
return err
}
}
return nil
}
func replaceTemplateData(file string, m map[string]interface{}) ([]byte, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("read file failed, err=%s", err)
}
if !bytes.Contains(data, []byte("{{")) {
return data, nil
}
builder := bytes.Buffer{}
tmpl, err := template.New(file).Parse(string(data))
if err != nil {
return nil, fmt.Errorf("parse data failed, err=%s", err)
}
err = tmpl.Execute(&builder, m)
if err != nil {
return nil, fmt.Errorf("execute data failed, err=%s", err)
}
return builder.Bytes(), nil
}
func replaceTemplateFilePath(file string, m map[string]interface{}) (string, error) {
if !strings.Contains(file, "{{") {
return file, nil
}
builder := strings.Builder{}
tmpl, err := template.New("file: " + file).Parse(file)
if err != nil {
return file, fmt.Errorf("parse file failed, err=%s", err)
}
err = tmpl.Execute(&builder, m)
if err != nil {
return file, fmt.Errorf("execute file failed, err=%s", err)
}
return builder.String(), nil
}
func trimExt(file string) string {
file = strings.TrimSuffix(file, ".tmpl")
file = strings.TrimSuffix(file, ".tpl")
file = strings.TrimSuffix(file, ".template")
return file
}
func (r *replacerInfo) isIgnoreFile(file string) bool {
isIgnore := false
for _, v := range r.ignoreFiles {
if isMatchFile(file, v) {
isIgnore = true
break
}
}
return isIgnore
}
func (r *replacerInfo) isInIgnoreDir(file string) bool {
isIgnore := false
dir, _ := filepath.Split(file)
for _, v := range r.ignoreDirs {
if strings.Contains(dir, v) {
isIgnore = true
break
}
}
return isIgnore
}
func isForbiddenFile(file string, path string) bool {
if gofile.IsWindows() {
path = strings.ReplaceAll(path, "/", "\\")
file = strings.ReplaceAll(file, "/", "\\")
}
return strings.Contains(file, path)
}
func (r *replacerInfo) getNewFilePath(file string) string {
//var newFilePath string
//if r.isActual {
// newFilePath = r.outPath + strings.Replace(file, r.path, "", 1)
//} else {
// newFilePath = r.outPath + strings.Replace(file, r.path, "", 1)
//}
newFilePath := r.outPath + strings.Replace(file, r.path, "", 1)
if gofile.IsWindows() {
newFilePath = strings.ReplaceAll(newFilePath, "/", "\\")
}
return newFilePath
}
func (r *replacerInfo) getNewFilePath2(file string, refDir string) string {
if refDir == "" {
return r.getNewFilePath(file)
}
newFilePath := r.outPath + gofile.GetPathDelimiter() + refDir + gofile.GetPathDelimiter() + strings.Replace(file, r.path, "", 1)
if gofile.IsWindows() {
newFilePath = strings.ReplaceAll(newFilePath, "/", "\\")
}
return newFilePath
}
// if windows, convert the path splitter
func (r *replacerInfo) convertPathDelimiter(filePath string) string {
if r.isActual && gofile.IsWindows() {
filePath = strings.ReplaceAll(filePath, "/", "\\")
}
return filePath
}
// if windows, batch convert path splitters
func (r *replacerInfo) convertPathsDelimiter(filePaths ...string) []string {
if r.isActual && gofile.IsWindows() {
filePathsTmp := []string{}
for _, dir := range filePaths {
filePathsTmp = append(filePathsTmp, strings.ReplaceAll(dir, "/", "\\"))
}
return filePathsTmp
}
return filePaths
}
func saveToNewFile(filePath string, data []byte) error {
// create directory
dir, _ := filepath.Split(filePath)
err := os.MkdirAll(dir, 0766)
if err != nil {
return err
}
// save file
err = os.WriteFile(filePath, data, 0666)
if err != nil {
return err
}
return nil
}
// iterates over all files in the embedded directory, returning the absolute path to the file
func listFiles(path string, fs embed.FS) ([]string, error) {
files := []string{}
err := walkDir(path, &files, fs)
return files, err
}
// iterating through the embedded catalog
func walkDir(dirPath string, allFiles *[]string, fs embed.FS) error {
files, err := fs.ReadDir(dirPath)
if err != nil {
return err
}
for _, file := range files {
deepFile := dirPath + "/" + file.Name()
if file.IsDir() {
_ = walkDir(deepFile, allFiles, fs)
continue
}
*allFiles = append(*allFiles, deepFile)
}
return nil
}
// determine if the first character of a string is a letter
func isFirstAlphabet(str string) bool {
if len(str) == 0 {
return false
}
if (str[0] >= 'A' && str[0] <= 'Z') || (str[0] >= 'a' && str[0] <= 'z') {
return true
}
return false
}
func isSubPath(filePath string, subPath string) bool {
dir, _ := filepath.Split(filePath)
return strings.Contains(dir, subPath)
}
func isMatchFile(filePath string, sf string) bool {
dir1, file1 := filepath.Split(filePath)
dir2, file2 := filepath.Split(sf)
if file1 != file2 {
return false
}
if gofile.IsWindows() {
dir1 = strings.ReplaceAll(dir1, "/", "\\")
dir2 = strings.ReplaceAll(dir2, "/", "\\")
} else {
dir1 = strings.ReplaceAll(dir1, "\\", "/")
dir2 = strings.ReplaceAll(dir2, "\\", "/")
}
l1, l2 := len(dir1), len(dir2)
if l1 >= l2 && dir1[l1-l2:] == dir2 {
return true
}
return false
}
package replacer
import (
"embed"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
//go:embed testDir
var fs embed.FS
func TestNewWithFS(t *testing.T) {
type args struct {
fn func() Replacer
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "New",
args: args{
fn: func() Replacer {
replacer, err := New("testDir")
if err != nil {
panic(err)
}
return replacer
},
},
wantErr: false,
},
{
name: "NewFS",
args: args{
fn: func() Replacer {
replacer, err := NewFS("testDir", fs)
if err != nil {
panic(err)
}
return replacer
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := tt.args.fn()
subDirs := []string{"testDir/replace"}
subFiles := []string{"testDir/foo.txt"}
ignoreDirs := []string{"testDir/ignore"}
ignoreFiles := []string{"test.txt"}
fields := []Field{
{
Old: "1234",
New: "....",
},
{
Old: "abcdef",
New: "hello_",
IsCaseSensitive: true,
},
}
r.SetSubDirsAndFiles(subDirs, subFiles...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreSubFiles(ignoreFiles...)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(fmt.Sprintf("%s/replacer_test/%s_%s",
os.TempDir(), tt.name, time.Now().Format("150405")))
_, err := r.ReadFile("replace.txt")
assert.NoError(t, err)
err = r.SaveFiles()
if (err != nil) != tt.wantErr {
t.Logf("SaveFiles() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Logf("save files successfully, out = %s", r.GetOutputDir())
})
}
}
func TestSaveTemplateFiles(t *testing.T) {
out := fmt.Sprintf("%s/replacer_test/template_%s", os.TempDir(), time.Now().Format("150405"))
m := map[string]interface{}{
"service": map[string]interface{}{"name": "user", "version": 1.0},
"port": 8080,
"isSecure": true,
}
r, err := New("testDir")
assert.NoError(t, err)
_ = r.SetOutputDir(out)
err = r.SaveTemplateFiles(m)
assert.NoError(t, err)
t.Log(out)
}
func TestReplacerError(t *testing.T) {
_, err := New("/notfound")
assert.Error(t, err)
_, err = NewFS("/notfound", embed.FS{})
assert.Error(t, err)
r, err := New("testDir")
assert.NoError(t, err)
r.SetIgnoreSubFiles()
r.SetSubDirsAndFiles(nil)
err = r.SetOutputDir("/tmp/yourServerName")
assert.NoError(t, err)
path := r.GetSourcePath()
assert.NotEmpty(t, path)
r = &replacerInfo{}
err = r.SaveFiles()
assert.NoError(t, err)
}
123
\ No newline at end of file
456
{{.service}}
{{.service.name}}
{{.service.version}}
{{.port}}
{{.isSecure}}
1234567890
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
test1
to do replace
{{.service}}
{{.port}}
{{.isSecure}}
package discovery
import (
"context"
"errors"
"strings"
"time"
"google.golang.org/grpc/resolver"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
const name = "discovery"
// Option is builder option.
type Option func(o *builder)
// WithTimeout with timeout option.
func WithTimeout(timeout time.Duration) Option {
return func(b *builder) {
b.timeout = timeout
}
}
// WithInsecure with isSecure option.
func WithInsecure(insecure bool) Option {
return func(b *builder) {
b.insecure = insecure
}
}
// DisableDebugLog disables update instances log.
func DisableDebugLog() Option {
return func(b *builder) {
b.debugLogDisabled = true
}
}
type builder struct {
discoverer registry.Discovery
timeout time.Duration
insecure bool
debugLogDisabled bool
}
// NewBuilder creates a builder which is used to factory registry resolvers.
func NewBuilder(d registry.Discovery, opts ...Option) resolver.Builder {
b := &builder{
discoverer: d,
timeout: time.Second * 10,
insecure: false,
debugLogDisabled: false,
}
for _, o := range opts {
o(b)
}
return b
}
func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) {
var (
err error
w registry.Watcher
)
done := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(context.Background())
go func() {
w, err = b.discoverer.Watch(ctx, strings.TrimPrefix(target.URL.Path, "/"))
close(done)
}()
select {
case <-done:
case <-time.After(b.timeout):
err = errors.New("discovery create watcher overtime")
}
if err != nil {
cancel()
return nil, err
}
r := &discoveryResolver{
w: w,
cc: cc,
ctx: ctx,
cancel: cancel,
insecure: b.insecure,
debugLogDisabled: b.debugLogDisabled,
}
go r.watch()
return r, nil
}
// Scheme return scheme of discovery
func (*builder) Scheme() string {
return name
}
package discovery
import (
"context"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/resolver"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
type discovery struct{}
func (d discovery) GetService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
return []*registry.ServiceInstance{}, nil
}
func (d discovery) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) {
return &watcher{}, nil
}
type watcher struct{}
func (w watcher) Next() ([]*registry.ServiceInstance, error) {
return []*registry.ServiceInstance{}, nil
}
func (w watcher) Stop() error {
return nil
}
func TestNewBuilder(t *testing.T) {
b := NewBuilder(&discovery{},
WithInsecure(false),
WithTimeout(time.Second),
DisableDebugLog(),
)
assert.NotNil(t, b)
}
func Test_builder_Build(t *testing.T) {
b := NewBuilder(&discovery{})
assert.NotNil(t, b)
u := url.URL{
Path: "ipv4.single.fake",
}
_, err := b.Build(resolver.Target{URL: u}, nil, resolver.BuildOptions{})
assert.NoError(t, err)
}
func Test_builder_Scheme(t *testing.T) {
b := NewBuilder(&discovery{})
assert.NotNil(t, b)
t.Log(b.Scheme())
}
// Package discovery is service discovery library, supports etcd, consul and nacos.
package discovery
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"time"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/resolver"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
type discoveryResolver struct {
w registry.Watcher
cc resolver.ClientConn
ctx context.Context
cancel context.CancelFunc
insecure bool
debugLogDisabled bool
}
func (r *discoveryResolver) watch() {
for {
select {
case <-r.ctx.Done():
return
default:
}
ins, err := r.w.Next()
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
fmt.Printf("[resolver] Failed to watch discovery endpoint: %v\n", err)
time.Sleep(time.Second)
continue
}
r.update(ins)
}
}
func (r *discoveryResolver) update(ins []*registry.ServiceInstance) {
addrs := make([]resolver.Address, 0)
endpoints := make(map[string]struct{})
for _, in := range ins {
endpoint, err := parseEndpoint(in.Endpoints, "grpc", !r.insecure)
if err != nil {
//fmt.Printf("[resolver] Failed to parse discovery endpoint: %v\n", err)
continue
}
if endpoint == "" {
continue
}
// filter redundant endpoints
if _, ok := endpoints[endpoint]; ok {
continue
}
endpoints[endpoint] = struct{}{}
addr := resolver.Address{
ServerName: in.Name,
Attributes: parseAttributes(in.Metadata),
Addr: endpoint,
}
addr.Attributes = addr.Attributes.WithValue("rawServiceInstance", in)
addrs = append(addrs, addr)
}
if len(addrs) == 0 {
//fmt.Printf("[resolver] Zero endpoint found,refused to write, instances: %v\n", ins)
return
}
err := r.cc.UpdateState(resolver.State{Addresses: addrs})
if err != nil {
fmt.Printf("[resolver] failed to update state: %v\n", err)
}
if !r.debugLogDisabled {
b, _ := json.Marshal(ins)
fmt.Printf("[resolver] update instances: %s\n", b)
}
}
func (r *discoveryResolver) Close() {
r.cancel()
err := r.w.Stop()
if err != nil {
fmt.Printf("[resolver] failed to watch top: %v\n", err)
}
}
func (r *discoveryResolver) ResolveNow(_ resolver.ResolveNowOptions) {}
func parseAttributes(md map[string]string) *attributes.Attributes {
var a *attributes.Attributes
for k, v := range md {
if a == nil {
a = attributes.New(k, v)
} else {
a = a.WithValue(k, v)
}
}
return a
}
// parseEndpoint parses an Endpoint URL.
func parseEndpoint(endpoints []string, scheme string, isSecure bool) (string, error) {
for _, e := range endpoints {
u, err := url.Parse(e)
if err != nil {
return "", err
}
if u.Scheme == scheme && IsSecure(u) == isSecure {
return u.Host, nil
}
}
return "", nil
}
// IsSecure parses isSecure for Endpoint URL.
func IsSecure(u *url.URL) bool {
ok, err := strconv.ParseBool(u.Query().Get("isSecure"))
if err != nil {
return false
}
return ok
}
package discovery
import (
"context"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
type cliConn struct {
}
func (c cliConn) UpdateState(state resolver.State) error {
return nil
}
func (c cliConn) ReportError(err error) {}
func (c cliConn) NewAddress(addresses []resolver.Address) {}
func (c cliConn) NewServiceConfig(serviceConfig string) {}
func (c cliConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
return &serviceconfig.ParseResult{}
}
func Test_discoveryResolver_Close(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
r := &discoveryResolver{
w: &watcher{},
cc: &cliConn{},
ctx: ctx,
cancel: cancel,
insecure: true,
debugLogDisabled: false,
}
defer r.Close()
r.ResolveNow(resolver.ResolveNowOptions{})
r.update([]*registry.ServiceInstance{registry.NewServiceInstance(
"foo",
"bar",
[]string{"grpc://127.0.0.1:8282"},
)})
//r.watch()
//time.Sleep(time.Millisecond * 100)
}
func Test_parseAttributes(t *testing.T) {
a := parseAttributes(map[string]string{"foo": "bar", "foo2": "bar2"})
assert.NotNil(t, a)
}
func Test_discoveryResolver_watch(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
r := &discoveryResolver{
w: &watcher{},
cc: &cliConn{},
ctx: ctx,
cancel: cancel,
insecure: true,
debugLogDisabled: false,
}
defer r.Close()
r.watch()
time.Sleep(time.Millisecond * 200)
}
func Test_parseEndpoint(t *testing.T) {
_, err := parseEndpoint([]string{"grpc://127.0.0.1:8282"}, "grpc", false)
assert.NoError(t, err)
_, err = parseEndpoint([]string{"grpc://127.0.0.1:8282"}, "grpc", true)
assert.NoError(t, err)
_, err = parseEndpoint(nil, "", true)
assert.NoError(t, err)
}
func TestIsSecure(t *testing.T) {
u, err := url.Parse("http://localhost:8080")
assert.NoError(t, err)
ok := IsSecure(u)
assert.Equal(t, false, ok)
u, _ = url.Parse("http://localhost:8080?isSecure=true")
ok = IsSecure(u)
assert.Equal(t, true, ok)
}
package consul
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/hashicorp/consul/api"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
// Client is consul client config
type Client struct {
client *api.Client
ctx context.Context
cancel context.CancelFunc
}
// NewClient creates consul client
func NewClient(cli *api.Client) *Client {
c := &Client{client: cli}
c.ctx, c.cancel = context.WithCancel(context.Background())
return c
}
// Service get services from consul
func (d *Client) Service(ctx context.Context, service string, index uint64, passingOnly bool) ([]*registry.ServiceInstance, uint64, error) {
opts := &api.QueryOptions{
WaitIndex: index,
WaitTime: time.Second * 55,
}
opts = opts.WithContext(ctx)
entries, meta, err := d.client.Health().Service(service, "", passingOnly, opts)
if err != nil {
return nil, 0, err
}
services := make([]*registry.ServiceInstance, 0)
for _, entry := range entries {
var version string
for _, tag := range entry.Service.Tags {
strs := strings.SplitN(tag, "=", 2)
if len(strs) == 2 && strs[0] == "version" {
version = strs[1]
}
}
var endpoints []string
for scheme, addr := range entry.Service.TaggedAddresses {
if scheme == "lan_ipv4" || scheme == "wan_ipv4" || scheme == "lan_ipv6" || scheme == "wan_ipv6" {
continue
}
endpoints = append(endpoints, addr.Address)
}
services = append(services, &registry.ServiceInstance{
ID: entry.Service.ID,
Name: entry.Service.Service,
Metadata: entry.Service.Meta,
Version: version,
Endpoints: endpoints,
OriginService: entry.Service,
})
}
return services, meta.LastIndex, nil
}
// Register register service instance to consul
func (d *Client) Register(_ context.Context, svc *registry.ServiceInstance, enableHealthCheck bool) error {
addresses := make(map[string]api.ServiceAddress)
var addr string
var port uint64
for _, endpoint := range svc.Endpoints {
raw, err := url.Parse(endpoint)
if err != nil {
return err
}
addr = raw.Hostname()
port, _ = strconv.ParseUint(raw.Port(), 10, 16)
addresses[raw.Scheme] = api.ServiceAddress{Address: endpoint, Port: int(port)}
}
asr := &api.AgentServiceRegistration{
ID: svc.ID,
Name: svc.Name,
Meta: svc.Metadata,
Tags: []string{fmt.Sprintf("version=%s", svc.Version)},
TaggedAddresses: addresses,
Address: addr,
Port: int(port),
}
if enableHealthCheck {
asr.Checks = append(asr.Checks, &api.AgentServiceCheck{
TCP: fmt.Sprintf("%s:%d", addr, port),
Interval: "20s",
Timeout: "5s",
Status: "passing",
DeregisterCriticalServiceAfter: "60s",
})
}
err := d.client.Agent().ServiceRegister(asr)
if err != nil {
return err
}
go func() {
ticker := time.NewTicker(time.Second * 20)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_ = d.client.Agent().UpdateTTL("service:"+svc.ID, "pass", "pass")
case <-d.ctx.Done():
return
}
}
}()
return nil
}
// Deregister deregister service by service ID
func (d *Client) Deregister(_ context.Context, serviceID string) error {
d.cancel()
return d.client.Agent().ServiceDeregister(serviceID)
}
package consul
import (
"context"
"testing"
"github.com/hashicorp/consul/api"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
func getConsulClient() *Client {
consulClient, err := api.NewClient(&api.Config{})
if err != nil {
panic(err)
}
return NewClient(consulClient)
}
func TestConsulClient(t *testing.T) {
cli := getConsulClient()
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
err := cli.Register(context.Background(), instance, false)
t.Log(err)
_, _, err = cli.Service(context.Background(), "foo", 1, false)
t.Log(err)
err = cli.Deregister(context.Background(), "1")
t.Log(err)
}
// Package consul is registered as a service using consul.
package consul
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/consul/api"
"gitlab.wanzhuangkj.com/tush/xpkg/consulcli"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
var (
_ registry.Registry = &Registry{}
_ registry.Discovery = &Registry{}
)
// Option is consul registry option.
type Option func(*Registry)
// WithHealthCheck with registry health check option.
func WithHealthCheck(enable bool) Option {
return func(o *Registry) {
o.enableHealthCheck = enable
}
}
// Config is consul registry config
type Config struct {
*api.Config
}
// Registry is consul registry
type Registry struct {
cli *Client
enableHealthCheck bool
registry map[string]*serviceSet
lock sync.RWMutex
}
// NewRegistry instantiating the consul registry
// Note: If the consulcli.WithConfig(*api.Config) parameter is set, the consulAddr parameter is ignored!
func NewRegistry(consulAddr string, id string, instanceName string, instanceEndpoints []string, opts ...consulcli.Option) (registry.Registry, *registry.ServiceInstance, error) {
serviceInstance := registry.NewServiceInstance(id, instanceName, instanceEndpoints)
cli, err := consulcli.Init(consulAddr, opts...)
if err != nil {
return nil, nil, err
}
return New(cli, WithHealthCheck(true)), serviceInstance, nil
}
// New create a consul registry
func New(apiClient *api.Client, opts ...Option) *Registry {
r := &Registry{
cli: NewClient(apiClient),
registry: make(map[string]*serviceSet),
enableHealthCheck: true,
}
for _, opt := range opts {
opt(r)
}
return r
}
// Register register service
func (r *Registry) Register(ctx context.Context, svc *registry.ServiceInstance) error {
return r.cli.Register(ctx, svc, r.enableHealthCheck)
}
// Deregister deregister service
func (r *Registry) Deregister(ctx context.Context, svc *registry.ServiceInstance) error {
// NOTE: invoke the func Deregister will block when err is not nil
return r.cli.Deregister(ctx, svc.ID)
}
// GetService return service by name
func (r *Registry) GetService(_ context.Context, name string) (services []*registry.ServiceInstance, err error) {
r.lock.RLock()
defer r.lock.RUnlock()
set := r.registry[name]
if set == nil {
return nil, fmt.Errorf("service %s not resolved in registry", name)
}
ss, _ := set.services.Load().([]*registry.ServiceInstance)
if ss == nil {
return nil, fmt.Errorf("service %s not found in registry", name)
}
services = append(services, ss...)
return //nolint
}
// ListServices return service list.
func (r *Registry) ListServices() (allServices map[string][]*registry.ServiceInstance, err error) {
r.lock.RLock()
defer r.lock.RUnlock()
allServices = make(map[string][]*registry.ServiceInstance)
for name, set := range r.registry {
var services []*registry.ServiceInstance
ss, _ := set.services.Load().([]*registry.ServiceInstance)
if ss == nil {
continue
}
services = append(services, ss...)
allServices[name] = services
}
return //nolint
}
// Watch resolve service by name
func (r *Registry) Watch(_ context.Context, name string) (registry.Watcher, error) {
r.lock.Lock()
defer r.lock.Unlock()
set, ok := r.registry[name]
if !ok {
set = &serviceSet{
watcher: make(map[*watcher]struct{}),
services: &atomic.Value{},
serviceName: name,
}
r.registry[name] = set
}
w := &watcher{
event: make(chan struct{}, 1),
}
w.ctx, w.cancel = context.WithCancel(context.Background())
w.set = set
set.lock.Lock()
set.watcher[w] = struct{}{}
set.lock.Unlock()
ss, _ := set.services.Load().([]*registry.ServiceInstance)
if len(ss) > 0 {
// If the service has a value, it needs to be pushed to the watcher,
// otherwise the initial data may be blocked forever during the watch.
w.event <- struct{}{}
}
if !ok {
go r.resolve(set)
}
return w, nil
}
func (r *Registry) resolve(ss *serviceSet) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
services, idx, err := r.cli.Service(ctx, ss.serviceName, 0, true)
cancel()
if err == nil && len(services) > 0 {
ss.broadcast(services)
}
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
<-ticker.C
ctx, cancel := context.WithTimeout(context.Background(), time.Second*120)
tmpService, tmpIdx, err := r.cli.Service(ctx, ss.serviceName, idx, true)
cancel()
if err != nil {
time.Sleep(time.Second)
continue
}
if len(tmpService) != 0 && tmpIdx != idx {
services = tmpService
ss.broadcast(services)
}
idx = tmpIdx
}
}
package consul
import (
"context"
"testing"
"time"
"github.com/hashicorp/consul/api"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
func TestNewRegistry(t *testing.T) {
consulAddr := "192.168.3.37:8500"
id := "serverName_192.168.3.37"
instanceName := "serverName"
instanceEndpoints := []string{"grpc://192.168.3.27:8282"}
iRegistry, serviceInstance, err := NewRegistry(consulAddr, id, instanceName, instanceEndpoints)
t.Log(err, iRegistry, serviceInstance)
}
func newConsulRegistry() *Registry {
consulClient, err := api.NewClient(&api.Config{})
if err != nil {
panic(err)
}
r := New(consulClient, WithHealthCheck(true))
return r
}
func TestRegistry_Register(t *testing.T) {
r := newConsulRegistry()
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
err := r.Register(context.Background(), instance)
t.Log(err)
_, err = r.ListServices()
t.Log(err)
_, err = r.GetService(context.Background(), "foo")
t.Log(err)
_, err = r.Watch(context.Background(), "foo")
t.Log(err)
go func() {
r.resolve(newServiceSet())
}()
err = r.Deregister(context.Background(), instance)
t.Log(err)
time.Sleep(time.Millisecond * 100)
}
package consul
import (
"sync"
"sync/atomic"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
type serviceSet struct {
serviceName string
watcher map[*watcher]struct{}
services *atomic.Value
lock sync.RWMutex
}
func (s *serviceSet) broadcast(ss []*registry.ServiceInstance) {
s.services.Store(ss)
s.lock.RLock()
defer s.lock.RUnlock()
for k := range s.watcher {
select {
case k.event <- struct{}{}:
default:
}
}
}
package consul
import (
"context"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
type watcher struct {
event chan struct{}
set *serviceSet
// for cancel
ctx context.Context
cancel context.CancelFunc
}
func (w *watcher) Next() (services []*registry.ServiceInstance, err error) {
select {
case <-w.ctx.Done():
err = w.ctx.Err()
case <-w.event:
}
ss, ok := w.set.services.Load().([]*registry.ServiceInstance)
if ok {
services = append(services, ss...)
}
return
}
func (w *watcher) Stop() error {
w.cancel()
w.set.lock.Lock()
defer w.set.lock.Unlock()
delete(w.set.watcher, w)
return nil
}
package consul
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
func newServiceSet() *serviceSet {
return &serviceSet{
serviceName: "foo",
watcher: map[*watcher]struct{}{},
services: &atomic.Value{},
lock: sync.RWMutex{},
}
}
func TestServiceSet_broadcast(t *testing.T) {
ss := newServiceSet()
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
ss.broadcast([]*registry.ServiceInstance{instance})
}
func newWatch() *watcher {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*2)
wt := &watcher{
event: make(chan struct{}),
set: newServiceSet(),
ctx: ctx,
cancel: cancelFunc,
}
return wt
}
func Test_watcher(t *testing.T) {
w := newWatch()
_, err := w.Next()
t.Log(err)
err = w.Stop()
t.Log(err)
}
package etcd
import (
"context"
"fmt"
"math/rand"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"gitlab.wanzhuangkj.com/tush/xpkg/etcdcli"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
var (
_ registry.Registry = &Registry{}
_ registry.Discovery = &Registry{}
)
// Option is etcd registry option.
type Option func(o *options)
type options struct {
ctx context.Context
namespace string
ttl time.Duration
maxRetry int
}
func defaultOptions() *options {
return &options{
ctx: context.Background(),
namespace: "/microservices",
ttl: time.Second * 15,
maxRetry: 5,
}
}
// WithContext with registry context.
func WithContext(ctx context.Context) Option {
return func(o *options) { o.ctx = ctx }
}
// WithNamespace with registry namespace.
func WithNamespace(ns string) Option {
return func(o *options) { o.namespace = ns }
}
// WithRegisterTTL with register ttl.
func WithRegisterTTL(ttl time.Duration) Option {
return func(o *options) { o.ttl = ttl }
}
// WithMaxRetry set max retry times.
func WithMaxRetry(num int) Option {
return func(o *options) { o.maxRetry = num }
}
// NewRegistry instantiating the etcd registry
// Note: If the etcdcli.WithConfig(*clientv3.Config) parameter is set, the etcdEndpoints parameter is ignored!
func NewRegistry(etcdEndpoints []string, id string, instanceName string, instanceEndpoints []string, opts ...etcdcli.Option) (registry.Registry, *registry.ServiceInstance, error) {
serviceInstance := registry.NewServiceInstance(id, instanceName, instanceEndpoints)
cli, err := etcdcli.Init(etcdEndpoints, opts...)
if err != nil {
return nil, nil, err
}
return New(cli), serviceInstance, nil
}
// Registry is etcd registry.
type Registry struct {
opts *options
client *clientv3.Client
kv clientv3.KV
lease clientv3.Lease
}
// New create a etcd registry
func New(client *clientv3.Client, opts ...Option) (r *Registry) {
o := defaultOptions()
for _, opt := range opts {
opt(o)
}
return &Registry{
opts: o,
client: client,
kv: clientv3.NewKV(client),
}
}
// Register the registration.
func (r *Registry) Register(ctx context.Context, service *registry.ServiceInstance) error {
key := fmt.Sprintf("%s/%s/%s", r.opts.namespace, service.Name, service.ID)
value, err := marshal(service)
if err != nil {
return err
}
if r.lease != nil {
_ = r.lease.Close()
}
r.lease = clientv3.NewLease(r.client)
leaseID, err := r.registerWithKV(ctx, key, value)
if err != nil {
return err
}
go r.heartBeat(r.opts.ctx, leaseID, key, value)
return nil
}
// Deregister the registration.
func (r *Registry) Deregister(ctx context.Context, service *registry.ServiceInstance) error {
defer func() {
if r.lease != nil {
_ = r.lease.Close()
}
}()
key := fmt.Sprintf("%s/%s/%s", r.opts.namespace, service.Name, service.ID)
_, err := r.client.Delete(ctx, key)
return err
}
// GetService return the service instances in memory according to the service name.
func (r *Registry) GetService(ctx context.Context, name string) ([]*registry.ServiceInstance, error) {
key := fmt.Sprintf("%s/%s", r.opts.namespace, name)
resp, err := r.kv.Get(ctx, key, clientv3.WithPrefix())
if err != nil {
return nil, err
}
items := make([]*registry.ServiceInstance, 0, len(resp.Kvs))
for _, kv := range resp.Kvs {
si, err := unmarshal(kv.Value)
if err != nil {
return nil, err
}
if si.Name != name {
continue
}
items = append(items, si)
}
return items, nil
}
// Watch creates a watcher according to the service name.
func (r *Registry) Watch(ctx context.Context, name string) (registry.Watcher, error) {
key := fmt.Sprintf("%s/%s", r.opts.namespace, name)
return newWatcher(ctx, key, name, r.client)
}
// registerWithKV create a new lease, return current leaseID
func (r *Registry) registerWithKV(ctx context.Context, key string, value string) (clientv3.LeaseID, error) {
grant, err := r.lease.Grant(ctx, int64(r.opts.ttl.Seconds()))
if err != nil {
return 0, err
}
_, err = r.client.Put(ctx, key, value, clientv3.WithLease(grant.ID))
if err != nil {
return 0, err
}
return grant.ID, nil
}
func (r *Registry) heartBeat(ctx context.Context, leaseID clientv3.LeaseID, key string, value string) {
curLeaseID := leaseID
kac, err := r.client.KeepAlive(ctx, leaseID)
if err != nil {
curLeaseID = 0
}
rand.Seed(time.Now().Unix()) //nolint
for {
if curLeaseID == 0 {
// try to registerWithKV
retreat := []int{}
for retryCnt := 0; retryCnt < r.opts.maxRetry; retryCnt++ {
if ctx.Err() != nil {
return
}
// prevent infinite blocking
idChan := make(chan clientv3.LeaseID, 1)
errChan := make(chan error, 1)
cancelCtx, cancel := context.WithCancel(ctx)
go func() {
defer cancel()
id, registerErr := r.registerWithKV(cancelCtx, key, value)
if registerErr != nil {
errChan <- registerErr
} else {
idChan <- id
}
}()
select {
case <-time.After(3 * time.Second):
cancel()
continue
case <-errChan:
continue
case curLeaseID = <-idChan:
}
kac, err = r.client.KeepAlive(ctx, curLeaseID)
if err == nil {
break
}
retreat = append(retreat, 1<<retryCnt)
time.Sleep(time.Duration(retreat[rand.Intn(len(retreat))]) * time.Second)
}
if _, ok := <-kac; !ok {
// retry failed
return
}
}
select {
case _, ok := <-kac:
if !ok {
if ctx.Err() != nil {
// channel closed due to context cancel
return
}
// need to retry registration
curLeaseID = 0
continue
}
case <-r.opts.ctx.Done():
return
}
}
}
package etcd
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
clientv3 "go.etcd.io/etcd/client/v3"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func TestNewRegistry(t *testing.T) {
etcdEndpoints := []string{"127.0.0.1:2379"}
id := "1"
instanceName := "serverName"
instanceEndpoints := []string{"grpc://127.0.0.1:8282"}
utils.SafeRunWithTimeout(time.Second*2, func(cancel context.CancelFunc) {
iRegistry, serviceInstance, err := NewRegistry(etcdEndpoints, id, instanceName, instanceEndpoints)
t.Log(err, iRegistry, serviceInstance)
})
}
type lease struct{}
func (l lease) Grant(ctx context.Context, ttl int64) (*clientv3.LeaseGrantResponse, error) {
return &clientv3.LeaseGrantResponse{}, nil
}
func (l lease) Revoke(ctx context.Context, id clientv3.LeaseID) (*clientv3.LeaseRevokeResponse, error) {
return &clientv3.LeaseRevokeResponse{}, nil
}
func (l lease) TimeToLive(ctx context.Context, id clientv3.LeaseID, opts ...clientv3.LeaseOption) (*clientv3.LeaseTimeToLiveResponse, error) {
return &clientv3.LeaseTimeToLiveResponse{}, nil
}
func (l lease) Leases(ctx context.Context) (*clientv3.LeaseLeasesResponse, error) {
return &clientv3.LeaseLeasesResponse{}, nil
}
func (l lease) KeepAlive(ctx context.Context, id clientv3.LeaseID) (<-chan *clientv3.LeaseKeepAliveResponse, error) {
c := make(chan *clientv3.LeaseKeepAliveResponse)
return c, nil
}
func (l lease) KeepAliveOnce(ctx context.Context, id clientv3.LeaseID) (*clientv3.LeaseKeepAliveResponse, error) {
return &clientv3.LeaseKeepAliveResponse{}, nil
}
func (l lease) Close() error {
return nil
}
type kv struct{}
func (k kv) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) {
return &clientv3.PutResponse{}, nil
}
func (k kv) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
return &clientv3.GetResponse{}, nil
}
func (k kv) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) {
return &clientv3.DeleteResponse{}, nil
}
func (k kv) Compact(ctx context.Context, rev int64, opts ...clientv3.CompactOption) (*clientv3.CompactResponse, error) {
return &clientv3.CompactResponse{}, nil
}
func (k kv) Do(ctx context.Context, op clientv3.Op) (clientv3.OpResponse, error) {
return clientv3.OpResponse{}, nil
}
func (k kv) Txn(ctx context.Context) clientv3.Txn {
return nil
}
func newEtcdRegistry() *Registry {
r := New(&clientv3.Client{Lease: &lease{}, KV: &kv{}},
WithRegisterTTL(time.Second),
WithContext(context.Background()),
WithMaxRetry(3),
WithNamespace("foo"),
)
r.lease = &lease{}
r.kv = &kv{}
return r
}
func TestRegistry_Register(t *testing.T) {
defer func() { recover() }()
r := newEtcdRegistry()
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
err := r.Register(context.Background(), instance)
assert.NoError(t, err)
}
func TestRegistry_Deregister(t *testing.T) {
r := newEtcdRegistry()
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
err := r.Deregister(context.Background(), instance)
assert.NoError(t, err)
}
func TestRegistry_GetService(t *testing.T) {
r := newEtcdRegistry()
_, err := r.GetService(context.Background(), "foo")
assert.NoError(t, err)
}
func TestRegistry_registerWithKV(t *testing.T) {
r := newEtcdRegistry()
_, err := r.registerWithKV(context.Background(), "foo", "bar")
assert.NoError(t, err)
}
func TestRegistry_heartBeat(t *testing.T) {
r := newEtcdRegistry()
go r.heartBeat(context.Background(), 1, "foo", "bar")
time.Sleep(time.Second)
}
// Package etcd is registered as a service using etcd.
package etcd
import (
"encoding/json"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
func marshal(si *registry.ServiceInstance) (string, error) {
data, err := json.Marshal(si)
if err != nil {
return "", err
}
return string(data), nil
}
// nolint
func unmarshal(data []byte) (si *registry.ServiceInstance, err error) {
err = json.Unmarshal(data, &si)
return
}
package etcd
import (
"context"
clientv3 "go.etcd.io/etcd/client/v3"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
var _ registry.Watcher = &watcher{}
type watcher struct {
key string
ctx context.Context
cancel context.CancelFunc
watchChan clientv3.WatchChan
watcher clientv3.Watcher
kv clientv3.KV
first bool
serviceName string
}
func newWatcher(ctx context.Context, key, name string, client *clientv3.Client) (*watcher, error) {
w := &watcher{
key: key,
first: true,
serviceName: name,
kv: clientv3.NewKV(client),
watcher: clientv3.NewWatcher(client),
}
w.ctx, w.cancel = context.WithCancel(ctx)
w.watchChan = w.watcher.Watch(w.ctx, key, clientv3.WithPrefix(), clientv3.WithRev(0))
err := w.watcher.RequestProgress(context.Background())
if err != nil {
return nil, err
}
return w, nil
}
func (w *watcher) Next() ([]*registry.ServiceInstance, error) {
if w.first {
item, err := w.getInstance()
w.first = false
return item, err
}
select {
case <-w.ctx.Done():
return nil, w.ctx.Err()
case <-w.watchChan:
return w.getInstance()
}
}
func (w *watcher) Stop() error {
w.cancel()
return w.watcher.Close()
}
func (w *watcher) getInstance() ([]*registry.ServiceInstance, error) {
resp, err := w.kv.Get(w.ctx, w.key, clientv3.WithPrefix())
if err != nil {
return nil, err
}
items := make([]*registry.ServiceInstance, 0, len(resp.Kvs))
for _, kv := range resp.Kvs {
si, err := unmarshal(kv.Value)
if err != nil {
return nil, err
}
if si.Name != w.serviceName {
continue
}
items = append(items, si)
}
return items, nil
}
package etcd
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
clientv3 "go.etcd.io/etcd/client/v3"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
type wt struct{}
func (w wt) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan {
c := make(chan clientv3.WatchResponse)
return c
}
func (w wt) RequestProgress(ctx context.Context) error {
return nil
}
func (w wt) Close() error {
return nil
}
func newWatch(first bool) *watcher {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*2)
r := New(&clientv3.Client{})
return &watcher{
key: "foo",
ctx: ctx,
cancel: cancelFunc,
watchChan: make(clientv3.WatchChan),
watcher: &wt{},
kv: r.kv,
first: first,
serviceName: "host",
}
}
func Test_watcher_Next(t *testing.T) {
w := newWatch(false)
instances, err := w.Next()
assert.Error(t, err)
t.Log(instances)
defer func() { recover() }()
w = newWatch(true)
instances, err = w.Next()
assert.Error(t, err)
t.Log(instances)
}
func Test_watcher_Stop(t *testing.T) {
w := newWatch(false)
err := w.Stop()
assert.NoError(t, err)
}
func Test_watcher_getInstance(t *testing.T) {
defer func() { recover() }()
w := newWatch(false)
instances, err := w.getInstance()
assert.NoError(t, err)
t.Log(instances)
}
func TestService_marshal(t *testing.T) {
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
v, err := marshal(instance)
assert.NoError(t, err)
si, err := unmarshal([]byte(v))
assert.NoError(t, err)
assert.Equal(t, instance, si)
}
// Package nacos is registered as a service using nacos.
package nacos
import (
"context"
"fmt"
"net"
"net/url"
"strconv"
"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"
"gitlab.wanzhuangkj.com/tush/xpkg/nacoscli"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
var (
_ registry.Registry = (*Registry)(nil)
_ registry.Discovery = (*Registry)(nil)
)
type options struct {
prefix string
weight float64
cluster string
group string
kind string
}
// Option is nacos option.
type Option func(o *options)
// WithPrefix with prefix path.
func WithPrefix(prefix string) Option {
return func(o *options) { o.prefix = prefix }
}
// WithWeight with weight option.
func WithWeight(weight float64) Option {
return func(o *options) { o.weight = weight }
}
// WithCluster with cluster option.
func WithCluster(cluster string) Option {
return func(o *options) { o.cluster = cluster }
}
// WithGroup with group option.
func WithGroup(group string) Option {
return func(o *options) { o.group = group }
}
// WithDefaultKind with default kind option.
func WithDefaultKind(kind string) Option {
return func(o *options) { o.kind = kind }
}
// Registry is nacos registry.
type Registry struct {
opts options
cli naming_client.INamingClient
}
// NewRegistry instantiating the nacos registry
func NewRegistry(nacosIPAddr string, nacosPort int, nacosNamespaceID string,
id string, instanceName string, instanceEndpoints []string,
opts ...nacoscli.Option) (registry.Registry, *registry.ServiceInstance, error) {
serviceInstance := registry.NewServiceInstance(id, instanceName, instanceEndpoints)
cli, err := nacoscli.NewNamingClient(nacosIPAddr, nacosPort, nacosNamespaceID, opts...)
if err != nil {
return nil, nil, err
}
return New(cli), serviceInstance, nil
}
// New new a nacos registry.
func New(cli naming_client.INamingClient, opts ...Option) (r *Registry) {
op := options{
prefix: "/microservices",
cluster: "DEFAULT",
group: constant.DEFAULT_GROUP,
weight: 100,
kind: "grpc",
}
for _, option := range opts {
option(&op)
}
return &Registry{
opts: op,
cli: cli,
}
}
// Register the registration.
func (r *Registry) Register(_ context.Context, si *registry.ServiceInstance) error {
if si.Name == "" {
return fmt.Errorf("nacos: serviceInstance.name can not be empty")
}
for _, endpoint := range si.Endpoints {
u, err := url.Parse(endpoint)
if err != nil {
return err
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
return err
}
p, err := strconv.Atoi(port)
if err != nil {
return err
}
var rmd map[string]string
if si.Metadata == nil {
rmd = map[string]string{
"id": si.ID,
"kind": u.Scheme,
"version": si.Version,
}
} else {
rmd = make(map[string]string, len(si.Metadata)+2)
for k, v := range si.Metadata {
rmd[k] = v
}
rmd["id"] = si.ID
rmd["kind"] = u.Scheme
rmd["version"] = si.Version
}
_, e := r.cli.RegisterInstance(vo.RegisterInstanceParam{
Ip: host,
Port: uint64(p),
ServiceName: si.Name + "." + u.Scheme,
Weight: r.opts.weight,
Enable: true,
Healthy: true,
Ephemeral: true,
Metadata: rmd,
ClusterName: r.opts.cluster,
GroupName: r.opts.group,
})
if e != nil {
return fmt.Errorf("RegisterInstance err %v, id = %s", e, si.ID)
}
}
return nil
}
// Deregister the registration.
func (r *Registry) Deregister(_ context.Context, service *registry.ServiceInstance) error {
for _, endpoint := range service.Endpoints {
u, err := url.Parse(endpoint)
if err != nil {
return err
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
return err
}
p, err := strconv.Atoi(port)
if err != nil {
return err
}
if _, err = r.cli.DeregisterInstance(vo.DeregisterInstanceParam{
Ip: host,
Port: uint64(p),
ServiceName: service.Name + "." + u.Scheme,
GroupName: r.opts.group,
Cluster: r.opts.cluster,
Ephemeral: true,
}); err != nil {
return err
}
}
return nil
}
// Watch creates a watcher according to the service name.
func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) {
return newWatcher(ctx, r.cli, serviceName, r.opts.group, r.opts.kind, []string{r.opts.cluster})
}
// GetService return the service instances in memory according to the service name.
func (r *Registry) GetService(_ context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
res, err := r.cli.SelectInstances(vo.SelectInstancesParam{
ServiceName: serviceName,
GroupName: r.opts.group,
HealthyOnly: true,
})
if err != nil {
return nil, err
}
items := make([]*registry.ServiceInstance, 0, len(res))
for _, in := range res {
kind := r.opts.kind
id := in.InstanceId
if in.Metadata != nil {
if k, ok := in.Metadata["kind"]; ok {
kind = k
}
if v, ok := in.Metadata["id"]; ok {
id = v
delete(in.Metadata, "id")
}
}
items = append(items, &registry.ServiceInstance{
ID: id,
Name: in.ServiceName,
Version: in.Metadata["version"],
Metadata: in.Metadata,
Endpoints: []string{fmt.Sprintf("%s://%s:%d", kind, in.Ip, in.Port)},
})
}
return items, nil
}
package nacos
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
func TestNewRegistry(t *testing.T) {
nacosIPAddr := "192.168.3.37"
nacosPort := 8848
nacosNamespaceID := "3454d2b5-2455-4d0e-bf6d-e033b086bb4c"
id := "serverName_192.168.3.37"
instanceName := "serverName"
instanceEndpoints := []string{"grpc://192.168.3.27:8282"}
utils.SafeRunWithTimeout(time.Second*2, func(cancel context.CancelFunc) {
iRegistry, instance, err := NewRegistry(nacosIPAddr, nacosPort, nacosNamespaceID, id, instanceName, instanceEndpoints)
if err != nil {
t.Log(err)
return
}
t.Log(iRegistry, instance)
})
}
func newNacosRegistry() *Registry {
return New(getCli(),
WithPrefix("/micro"),
WithWeight(1),
WithCluster("cluster"),
WithGroup("dev"),
WithDefaultKind("grpc"),
)
}
func TestRegistry(t *testing.T) {
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
r := &Registry{}
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
r = newNacosRegistry()
})
go func() {
defer func() { recover() }()
_, err := r.Watch(context.Background(), "foo")
t.Log(err)
}()
defer func() { recover() }()
time.Sleep(time.Millisecond * 10)
err := r.Register(context.Background(), instance)
t.Log(err)
}
func TestDeregister(t *testing.T) {
instance := registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"})
r := &Registry{}
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
r = newNacosRegistry()
})
defer func() { recover() }()
err := r.Deregister(context.Background(), instance)
t.Log(err)
}
func TestGetService(t *testing.T) {
r := &Registry{}
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
r = newNacosRegistry()
})
defer func() { recover() }()
_, err := r.GetService(context.Background(), "foo")
t.Log(err)
}
func TestRegistry_RegisterError(t *testing.T) {
instance := registry.NewServiceInstance("", "", []string{"grpc://127.0.0.1:8282"})
r := &Registry{}
utils.SafeRunWithTimeout(time.Second*3, func(cancel context.CancelFunc) {
r = newNacosRegistry()
})
defer func() { recover() }()
err := r.Register(context.Background(), instance)
assert.Error(t, err)
instance = registry.NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.2:8282"},
registry.WithMetadata(map[string]string{
"foo2": "bar2",
}))
err = r.Register(context.Background(), instance)
assert.Error(t, err)
instance = registry.NewServiceInstance("foo", "bar", []string{"127.0.0.1:port"})
err = r.Register(context.Background(), instance)
assert.Error(t, err)
instance = registry.NewServiceInstance("foo", "bar", []string{"127.0.0.1"})
err = r.Register(context.Background(), instance)
assert.Error(t, err)
}
package nacos
import (
"context"
"fmt"
"github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client"
"github.com/nacos-group/nacos-sdk-go/v2/model"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"gitlab.wanzhuangkj.com/tush/xpkg/servicerd/registry"
)
var _ registry.Watcher = (*watcher)(nil)
type watcher struct {
serviceName string
clusters []string
groupName string
ctx context.Context
cancel context.CancelFunc
watchChan chan struct{}
cli naming_client.INamingClient
kind string
}
func newWatcher(ctx context.Context, cli naming_client.INamingClient, serviceName, groupName, kind string, clusters []string) (*watcher, error) {
w := &watcher{
serviceName: serviceName,
clusters: clusters,
groupName: groupName,
cli: cli,
kind: kind,
watchChan: make(chan struct{}, 1),
}
w.ctx, w.cancel = context.WithCancel(ctx)
e := w.cli.Subscribe(&vo.SubscribeParam{
ServiceName: serviceName,
GroupName: groupName,
//Clusters: clusters, // if set the clusters, subscription messages cannot be received
SubscribeCallback: func(services []model.Instance, err error) {
w.watchChan <- struct{}{}
},
})
return w, e
}
func (w *watcher) Next() ([]*registry.ServiceInstance, error) {
select {
case <-w.ctx.Done():
return nil, w.ctx.Err()
case <-w.watchChan:
}
res, err := w.cli.GetService(vo.GetServiceParam{
ServiceName: w.serviceName,
GroupName: w.groupName,
//Clusters: w.clusters, // if cluster is set, the latest service is not obtained.
})
if err != nil {
return nil, err
}
items := make([]*registry.ServiceInstance, 0, len(res.Hosts))
for _, in := range res.Hosts {
kind := w.kind
id := in.InstanceId
if in.Metadata != nil {
if k, ok := in.Metadata["kind"]; ok {
kind = k
}
if v, ok := in.Metadata["id"]; ok {
id = v
delete(in.Metadata, "id")
}
}
items = append(items, &registry.ServiceInstance{
ID: id,
Name: res.Name,
Version: in.Metadata["version"],
Metadata: in.Metadata,
Endpoints: []string{fmt.Sprintf("%s://%s:%d", kind, in.Ip, in.Port)},
})
}
return items, nil
}
func (w *watcher) Stop() error {
w.cancel()
return w.cli.Unsubscribe(&vo.SubscribeParam{
ServiceName: w.serviceName,
GroupName: w.groupName,
Clusters: w.clusters,
})
}
package nacos
import (
"context"
"testing"
"time"
"github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client"
"github.com/stretchr/testify/assert"
"gitlab.wanzhuangkj.com/tush/xpkg/nacoscli"
)
func getCli() naming_client.INamingClient {
var (
ipAddr = "192.168.3.37"
port = 8848
namespaceID = "3454d2b5-2455-4d0e-bf6d-e033b086bb4c"
)
namingClient, err := nacoscli.NewNamingClient(ipAddr, port, namespaceID)
if err != nil {
panic(err)
}
return namingClient
}
func newWatch() *watcher {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*2)
wt := &watcher{
serviceName: "host",
clusters: []string{"bar"},
groupName: "foo",
ctx: ctx,
cancel: cancelFunc,
watchChan: make(chan struct{}),
cli: getCli(),
kind: "host",
}
return wt
}
func Test_newWatcher(t *testing.T) {
defer func() { recover() }()
_, _ = newWatcher(context.Background(), getCli(), "host", "host", "foo", []string{"bar"})
}
func Test_watcher(t *testing.T) {
defer func() { recover() }()
_, _ = newWatcher(context.Background(), getCli(), "host", "host", "foo", []string{"bar"})
w := newWatch()
_, err := w.Next()
t.Log(err)
err = w.Stop()
assert.NoError(t, err)
}
package registry
// Option service instance options
type Option func(*options)
type options struct {
version string
metadata map[string]string
}
func defaultOptions() *options {
return &options{}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithVersion set server version
func WithVersion(version string) Option {
return func(o *options) {
o.version = version
}
}
// WithMetadata set metadata
func WithMetadata(metadata map[string]string) Option {
return func(o *options) {
o.metadata = metadata
}
}
// Package registry is service registry library, supports etcd, consul and nacos.
package registry
import "context"
// Registry is service registrar.
type Registry interface {
// Register the registration.
Register(ctx context.Context, service *ServiceInstance) error
// Deregister the registration.
Deregister(ctx context.Context, service *ServiceInstance) error
}
// Discovery is service discovery.
type Discovery interface {
// GetService return the service instances in memory according to the service name.
GetService(ctx context.Context, serviceName string) ([]*ServiceInstance, error)
// Watch creates a watcher according to the service name.
Watch(ctx context.Context, serviceName string) (Watcher, error)
}
// Watcher is service watcher.
type Watcher interface {
// Next returns services in the following two cases:
// 1.the first time to watch and the service instance list is not empty.
// 2.any service instance changes found.
// if the above two conditions are not met, it will block until context deadline exceeded or canceled
Next() ([]*ServiceInstance, error)
// Stop close the watcher.
Stop() error
}
// ServiceInstance is an instance of a service in a discovery system.
type ServiceInstance struct {
// ID is the unique instance ID as registered.
ID string `json:"id" example:"101"`
// Name is the service name as registered.
Name string `json:"name"`
// Version is the version of the compiled.
Version string `json:"version"`
// Metadata is the kv pair metadata associated with the service instance.
Metadata map[string]string `json:"metadata"`
// Endpoints is endpoint addresses of the service instance.
// schema:
// http://127.0.0.1:8000?isSecure=false
// grpc://127.0.0.1:9000?isSecure=false
Endpoints []string `json:"endpoints"`
OriginService any
}
// NewServiceInstance creates a new instance
func NewServiceInstance(id string, name string, endpoints []string, opts ...Option) *ServiceInstance {
o := defaultOptions()
o.apply(opts...)
return &ServiceInstance{
ID: id,
Name: name,
Endpoints: endpoints,
Version: o.version,
Metadata: o.metadata,
}
}
package registry
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewServiceInstance(t *testing.T) {
s := NewServiceInstance("foo", "bar", []string{"grpc://127.0.0.1:8282"},
WithVersion("v1.0.0"),
WithMetadata(map[string]string{"foo": "bar"}),
)
assert.NotNil(t, s)
}
package parser
import (
"encoding/json"
"fmt"
"strings"
"text/template"
"github.com/jinzhu/inflection"
)
// CrudInfo crud info for cache, dao, handler, service, protobuf, error
type CrudInfo struct {
TableNameCamel string `json:"tableNameCamel"` // camel case, example: FooBar
TableNameCamelFCL string `json:"tableNameCamelFCL"` // camel case and first character lower, example: fooBar
TableNamePluralCamel string `json:"tableNamePluralCamel"` // plural, camel case, example: FooBars
TableNamePluralCamelFCL string `json:"tableNamePluralCamelFCL"` // plural, camel case, example: fooBars
ColumnName string `json:"columnName"` // column name, example: first_name
ColumnNameCamel string `json:"columnNameCamel"` // column name, camel case, example: FirstName
ColumnNameCamelFCL string `json:"columnNameCamelFCL"` // column name, camel case and first character lower, example: firstName
ColumnNamePluralCamel string `json:"columnNamePluralCamel"` // column name, plural, camel case, example: FirstNames
ColumnNamePluralCamelFCL string `json:"columnNamePluralCamelFCL"` // column name, plural, camel case and first character lower, example: firstNames
GoType string `json:"goType"` // go type, example: string, uint64
GoTypeFCU string `json:"goTypeFCU"` // go type, first character upper, example: String, Uint64
ProtoType string `json:"protoType"` // proto type, example: string, uint64
IsStringType bool `json:"isStringType"` // go type is string or not
PrimaryKeyColumnName string `json:"PrimaryKeyColumnName"` // primary key, example: id
IsCommonType bool `json:"isCommonType"` // custom primary key name and type
IsStandardPrimaryKey bool `json:"isStandardPrimaryKey"` // standard primary key id
}
func isDesiredGoType(t string) bool {
switch t {
case "string", "uint64", "int64", "uint", "int", "uint32", "int32": //nolint
return true
}
return false
}
func setCrudInfo(field tmplField) *CrudInfo {
primaryKeyName := ""
if field.IsPrimaryKey {
primaryKeyName = field.ColName
}
pluralName := inflection.Plural(field.Name)
return &CrudInfo{
ColumnName: field.ColName,
ColumnNameCamel: field.Name,
ColumnNameCamelFCL: customFirstLetterToLower(field.Name),
ColumnNamePluralCamel: customEndOfLetterToLower(field.Name, pluralName),
ColumnNamePluralCamelFCL: customFirstLetterToLower(customEndOfLetterToLower(field.Name, pluralName)),
GoType: field.GoType,
GoTypeFCU: firstLetterToUpper(field.GoType),
ProtoType: simpleGoTypeToProtoType(field.GoType),
IsStringType: field.GoType == "string",
PrimaryKeyColumnName: primaryKeyName,
IsStandardPrimaryKey: field.ColName == "id",
}
}
func newCrudInfo(data tmplData) *CrudInfo {
if len(data.Fields) == 0 {
return nil
}
var info *CrudInfo
for _, field := range data.Fields {
if field.IsPrimaryKey {
info = setCrudInfo(field)
break
}
}
// if not found primary key, find the first xxx_id column as primary key
if info == nil {
for _, field := range data.Fields {
if strings.HasSuffix(field.ColName, "_id") && isDesiredGoType(field.GoType) { // xxx_id
info = setCrudInfo(field)
break
}
}
}
// if not found xxx_id field, use the first field of integer or string type
if info == nil {
for _, field := range data.Fields {
if isDesiredGoType(field.GoType) {
info = setCrudInfo(field)
break
}
}
}
// use the first column as primary key
if info == nil {
info = setCrudInfo(data.Fields[0])
}
info.TableNameCamel = data.TableName
info.TableNameCamelFCL = data.TName
pluralName := inflection.Plural(data.TableName)
info.TableNamePluralCamel = customEndOfLetterToLower(data.TableName, pluralName)
info.TableNamePluralCamelFCL = customFirstLetterToLower(customEndOfLetterToLower(data.TableName, pluralName))
return info
}
func (info *CrudInfo) getCode() string {
if info == nil {
return ""
}
pkData, _ := json.Marshal(info)
return string(pkData)
}
func (info *CrudInfo) CheckCommonType() bool {
if info == nil {
return false
}
return info.IsCommonType
}
func (info *CrudInfo) isIDPrimaryKey() bool {
if info == nil {
return false
}
if info.ColumnName == "id" && (info.GoType == "uint64" ||
info.GoType == "int64" ||
info.GoType == "uint" ||
info.GoType == "int" ||
info.GoType == "uint32" ||
info.GoType == "int32") {
return true
}
return false
}
func (info *CrudInfo) GetGRPCProtoValidation() string {
if info == nil {
return ""
}
if info.ProtoType == "string" {
return `[(validate.rules).string.min_len = 1]`
}
return fmt.Sprintf(`[(validate.rules).%s.gt = 0]`, info.ProtoType)
}
func (info *CrudInfo) GetWebProtoValidation() string {
if info == nil {
return ""
}
if info.ProtoType == "string" {
return fmt.Sprintf(`[(validate.rules).string.min_len = 1, (tagger.tags) = "uri:\"%s\""]`, info.ColumnNameCamelFCL)
}
return fmt.Sprintf(`[(validate.rules).%s.gt = 0, (tagger.tags) = "uri:\"%s\""]`, info.ProtoType, info.ColumnNameCamelFCL)
}
func getCommonHandlerStructCodes(data tmplData, jsonNamedType int) (string, error) {
newFields := []tmplField{}
for _, field := range data.Fields {
if jsonNamedType == 0 { // snake case
field.JSONName = customToSnake(field.ColName)
} else {
field.JSONName = customToCamel(field.ColName) // camel case (default)
}
newFields = append(newFields, field)
}
data.Fields = newFields
postStructCode, err := tmplExecuteWithFilter(data, handlerCreateStructCommonTmpl)
if err != nil {
return "", fmt.Errorf("handlerCreateStructTmpl error: %v", err)
}
putStructCode, err := tmplExecuteWithFilter(data, handlerUpdateStructCommonTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handlerUpdateStructTmpl error: %v", err)
}
getStructCode, err := tmplExecuteWithFilter(data, handlerDetailStructCommonTmpl, columnID, columnCreatedAt, columnUpdatedAt)
if err != nil {
return "", fmt.Errorf("handlerDetailStructTmpl error: %v", err)
}
return postStructCode + putStructCode + getStructCode, nil
}
func getCommonServiceStructCode(data tmplData) (string, error) {
builder := strings.Builder{}
err := serviceStructCommonTmpl.Execute(&builder, data)
if err != nil {
return "", err
}
code := builder.String()
serviceCreateStructCode, err := tmplExecuteWithFilter(data, serviceCreateStructCommonTmpl)
if err != nil {
return "", fmt.Errorf("handle serviceCreateStructTmpl error: %v", err)
}
serviceCreateStructCode = strings.ReplaceAll(serviceCreateStructCode, "ID:", "Id:")
serviceUpdateStructCode, err := tmplExecuteWithFilter(data, serviceUpdateStructCommonTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handle serviceUpdateStructTmpl error: %v", err)
}
serviceUpdateStructCode = strings.ReplaceAll(serviceUpdateStructCode, "ID:", "Id:")
code = strings.ReplaceAll(code, "// serviceCreateStructCode", serviceCreateStructCode)
code = strings.ReplaceAll(code, "// serviceUpdateStructCode", serviceUpdateStructCode)
return code, nil
}
func getCommonProtoFileCode(data tmplData, jsonNamedType int, isWebProto bool, isExtendedAPI bool) (string, error) {
data.Fields = goTypeToProto(data.Fields, jsonNamedType, true)
var err error
builder := strings.Builder{}
if isWebProto {
if isExtendedAPI {
err = protoFileForWebCommonTmpl.Execute(&builder, data)
} else {
err = protoFileForSimpleWebCommonTmpl.Execute(&builder, data)
}
if err != nil {
return "", err
}
} else {
if isExtendedAPI {
err = protoFileCommonTmpl.Execute(&builder, data)
} else {
err = protoFileSimpleCommonTmpl.Execute(&builder, data)
}
if err != nil {
return "", err
}
}
code := builder.String()
protoMessageCreateCode, err := tmplExecuteWithFilter2(data, protoMessageCreateCommonTmpl)
if err != nil {
return "", fmt.Errorf("handle protoMessageCreateCommonTmpl error: %v", err)
}
protoMessageUpdateCode, err := tmplExecuteWithFilter2(data, protoMessageUpdateCommonTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handle protoMessageUpdateCommonTmpl error: %v", err)
}
if !isWebProto {
srcStr := fmt.Sprintf(`, (tagger.tags) = "uri:\"%s\""`, getProtoFieldName(data.Fields))
protoMessageUpdateCode = strings.ReplaceAll(protoMessageUpdateCode, srcStr, "")
}
protoMessageDetailCode, err := tmplExecuteWithFilter2(data, protoMessageDetailCommonTmpl, columnID, columnCreatedAt, columnUpdatedAt)
if err != nil {
return "", fmt.Errorf("handle protoMessageDetailCommonTmpl error: %v", err)
}
code = strings.ReplaceAll(code, "// protoMessageCreateCode", protoMessageCreateCode)
code = strings.ReplaceAll(code, "// protoMessageUpdateCode", protoMessageUpdateCode)
code = strings.ReplaceAll(code, "// protoMessageDetailCode", protoMessageDetailCode)
code = strings.ReplaceAll(code, "*time.Time", "int64")
code = strings.ReplaceAll(code, "time.Time", "int64")
code = strings.ReplaceAll(code, "left_curly_bracket", "{")
code = strings.ReplaceAll(code, "right_curly_bracket", "}")
code = adaptedDbType2(data, isWebProto, code)
return code, nil
}
func tmplExecuteWithFilter2(data tmplData, tmpl *template.Template, reservedColumns ...string) (string, error) {
var newFields = []tmplField{}
for _, field := range data.Fields {
if isIgnoreFields(field.ColName, reservedColumns...) {
continue
}
newFields = append(newFields, field)
}
data.Fields = newFields
builder := strings.Builder{}
err := tmpl.Execute(&builder, data)
if err != nil {
return "", fmt.Errorf("tmpl.Execute error: %v", err)
}
return builder.String(), nil
}
// nolint
func simpleGoTypeToProtoType(goType string) string {
var protoType string
switch goType {
case "int", "int32":
protoType = "int32"
case "uint", "uint32":
protoType = "uint32"
case "int64":
protoType = "int64"
case "uint64":
protoType = "uint64"
case "string":
protoType = "string"
case "time.Time", "*time.Time":
protoType = "string"
case "float32":
protoType = "float"
case "float64":
protoType = "double"
case goTypeInts, "[]int64":
protoType = "repeated int64"
case "[]int32":
protoType = "repeated int32"
case "[]byte":
protoType = "string"
case goTypeStrings:
protoType = "repeated string"
case jsonTypeName:
protoType = "string"
default:
protoType = "string"
}
return protoType
}
func adaptedDbType2(data tmplData, isWebProto bool, code string) string {
if isWebProto {
code = replaceProtoMessageFieldCode(code, webDefaultProtoMessageFieldCodes)
} else {
code = replaceProtoMessageFieldCode(code, grpcDefaultProtoMessageFieldCodes)
}
if data.ProtoSubStructs != "" {
code += "\n" + data.ProtoSubStructs
}
return code
}
func firstLetterToUpper(str string) string {
if len(str) == 0 {
return str
}
if (str[0] >= 'A' && str[0] <= 'Z') || (str[0] >= 'a' && str[0] <= 'z') {
return strings.ToUpper(str[:1]) + str[1:]
}
return str
}
func customFirstLetterToLower(str string) string {
str = firstLetterToLower(str)
if len(str) == 2 {
if str == "iD" {
str = "id"
} else if str == "iP" {
str = "ip"
}
} else if len(str) == 3 {
if str == "iDs" {
str = "ids"
} else if str == "iPs" {
str = "ips"
}
}
return str
}
func customEndOfLetterToLower(srcStr string, str string) string {
l := len(str) - len(srcStr)
if l == 1 {
if str[len(str)-1] == 'S' {
return str[:len(str)-1] + "s"
}
} else if l == 2 {
if str[len(str)-2:] == "ES" {
return str[:len(str)-2] + "es"
}
}
return str
}
package parser
import (
"sync"
"text/template"
"github.com/pkg/errors"
)
// nolint
var (
handlerCreateStructCommonTmpl *template.Template
handlerCreateStructCommonTmplRaw = `
// Create{{.TableName}}Request request params
type Create{{.TableName}}Request struct {
{{- range .Fields}}
{{.Name}} {{.GoType}} ` + "`" + `json:"{{.JSONName}}" binding:""` + "`" + `{{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
`
handlerUpdateStructCommonTmpl *template.Template
handlerUpdateStructCommonTmplRaw = `
// Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request request params
type Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request struct {
{{- range .Fields}}
{{.Name}} {{.GoType}} ` + "`" + `json:"{{.JSONName}}" binding:""` + "`" + `{{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
`
handlerDetailStructCommonTmpl *template.Template
handlerDetailStructCommonTmplRaw = `
// {{.TableName}}ObjDetail detail
type {{.TableName}}ObjDetail struct {
{{- range .Fields}}
{{.Name}} {{.GoType}} ` + "`" + `json:"{{.JSONName}}"` + "`" + `{{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}`
protoFileCommonTmpl *template.Template
protoFileCommonTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {}
// delete {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc DeleteBy{{.CrudInfo.ColumnNameCamel}}(Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {}
// update {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc UpdateBy{{.CrudInfo.ColumnNameCamel}}(Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {}
// get {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc GetBy{{.CrudInfo.ColumnNameCamel}}(Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {}
// delete {{.TName}} by batch {{.CrudInfo.ColumnNameCamelFCL}}
rpc DeleteBy{{.CrudInfo.ColumnNamePluralCamel}}(Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request) returns (Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply) {}
// get {{.TName}} by condition
rpc GetSliceByCondition(Get{{.TableName}}ByConditionRequest) returns (Get{{.TableName}}ByConditionReply) {}
// list of {{.TName}} by batch {{.CrudInfo.ColumnNameCamelFCL}}
rpc ListBy{{.CrudInfo.ColumnNamePluralCamel}}(List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request) returns (List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply) {}
// list {{.TName}} by last {{.CrudInfo.ColumnNameCamelFCL}}
rpc ListByLast{{.CrudInfo.ColumnNameCamel}}(List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Request) returns (List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Reply) {}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1;
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetGRPCProtoValidation}};
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageUpdateCode
message Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageDetailCode
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetGRPCProtoValidation}};
}
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 2;
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request {
repeated {{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNamePluralCamelFCL}} = 1 [(validate.rules).repeated.min_items = 1];
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply {
}
message Get{{.TableName}}ByConditionRequest {
types.Conditions conditions = 1;
}
message Get{{.TableName}}ByConditionReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request {
repeated {{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNamePluralCamelFCL}} = 1 [(validate.rules).repeated.min_items = 1];
}
message List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply {
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 1;
}
message List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} last{{.CrudInfo.ColumnNameCamel}} = 1;
uint32 limit = 2 [(validate.rules).uint32.gt = 0]; // limit size per page
string sort = 3; // sort by column name of table, default is -{{.CrudInfo.ColumnName}}, the - sign indicates descending order.
}
message List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Reply {
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 1;
}
`
protoFileSimpleCommonTmpl *template.Template
protoFileSimpleCommonTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {}
// delete {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc DeleteBy{{.CrudInfo.ColumnNameCamel}}(Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {}
// update {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc UpdateBy{{.CrudInfo.ColumnNameCamel}}(Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {}
// get {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc GetBy{{.CrudInfo.ColumnNameCamel}}(Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1;
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetGRPCProtoValidation}};
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageUpdateCode
message Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageDetailCode
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetGRPCProtoValidation}};
}
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 2;
}
`
protoFileForWebCommonTmpl *template.Template
protoFileForWebCommonTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "tagger/tagger.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
/*
Reference https://github.com/grpc-ecosystem/grpc-gateway/blob/db7fbefff7c04877cdb32e16d4a248a024428207/examples/internal/proto/examplepb/a_bit_of_everything.proto
Default settings for generating swagger documents
NOTE: because json does not support 64 bits, the int64 and uint64 types under *.swagger.json are automatically converted to string types
Tips: add swagger option to rpc method, example:
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "get user by id",
description: "get user by id",
security: {
security_requirement: {
key: "BearerAuth";
value: {}
}
}
};
*/
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
host: "localhost:8080"
base_path: ""
info: {
title: "serverNameExample api docs";
version: "2.0";
}
schemes: HTTP;
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
security_definitions: {
security: {
key: "BearerAuth";
value: {
type: TYPE_API_KEY;
in: IN_HEADER;
name: "Authorization";
description: "Type Bearer your-jwt-token to Value";
}
}
}
};
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}"
body: "*"
};
}
// delete {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc DeleteBy{{.CrudInfo.ColumnNameCamel}}(Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {
option (google.api.http) = {
delete: "/api/v1/{{.TName}}/left_curly_bracket{{.CrudInfo.ColumnNameCamelFCL}}right_curly_bracket"
};
}
// update {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc UpdateBy{{.CrudInfo.ColumnNameCamel}}(Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {
option (google.api.http) = {
put: "/api/v1/{{.TName}}/left_curly_bracket{{.CrudInfo.ColumnNameCamelFCL}}right_curly_bracket"
body: "*"
};
}
// get {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc GetBy{{.CrudInfo.ColumnNameCamel}}(Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {
option (google.api.http) = {
get: "/api/v1/{{.TName}}/left_curly_bracket{{.CrudInfo.ColumnNameCamelFCL}}right_curly_bracket"
};
}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/list"
body: "*"
};
}
// delete {{.TName}} by batch {{.CrudInfo.ColumnNameCamelFCL}}
rpc DeleteBy{{.CrudInfo.ColumnNamePluralCamel}}(Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request) returns (Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/delete/ids"
body: "*"
};
}
// get {{.TName}} by condition
rpc GetSliceByCondition(Get{{.TableName}}ByConditionRequest) returns (Get{{.TableName}}ByConditionReply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/condition"
body: "*"
};
}
// list of {{.TName}} by batch {{.CrudInfo.ColumnNameCamelFCL}}
rpc ListBy{{.CrudInfo.ColumnNamePluralCamel}}(List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request) returns (List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/list/ids"
body: "*"
};
}
// list {{.TName}} by last {{.CrudInfo.ColumnNameCamelFCL}}
rpc ListByLast{{.CrudInfo.ColumnNameCamel}}(List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Request) returns (List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Reply) {
option (google.api.http) = {
get: "/api/v1/{{.TName}}/list"
};
}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
If used to generate code that supports the HTTP protocol, notes for defining message fields:
1. If the route contains the path parameter, such as /api/v1/userExample/{id}, the defined
message must contain the name of the path parameter and the name should be added
with a new tag, such as int64 id = 1 [(tagger.tags) = "uri:\"id\""];
2. If the request url is followed by a query parameter, such as /api/v1/getUserExample?name=Tom,
a form tag must be added when defining the query parameter in the message, such as:
string name = 1 [(tagger.tags) = "form:\"name\""].
3. If the message field name contain underscores(such as 'field_name'), it will cause a problem
where the JSON field names of the Swagger request parameters are different from those of the
GRPC JSON tag names. There are two solutions: Solution 1, remove the underline from the
message field name. Option 2, use the tool 'protoc-go-inject-tag' to modify the JSON tag name,
such as: string first_name = 1 ; // @gotags: json:"firstName"
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1;
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetWebProtoValidation}};
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageUpdateCode
message Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageDetailCode
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetWebProtoValidation}};
}
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 2;
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request {
repeated {{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNamePluralCamelFCL}} = 1 [(validate.rules).repeated.min_items = 1];
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply {
}
message Get{{.TableName}}ByConditionRequest {
types.Conditions conditions = 1;
}
message Get{{.TableName}}ByConditionReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Request {
repeated {{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNamePluralCamelFCL}} = 1 [(validate.rules).repeated.min_items = 1];
}
message List{{.TableName}}By{{.CrudInfo.ColumnNamePluralCamel}}Reply {
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 1;
}
message List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} last{{.CrudInfo.ColumnNameCamel}} = 1 [(tagger.tags) = "form:\"last{{.CrudInfo.ColumnNameCamel}}\""];
uint32 limit = 2 [(validate.rules).uint32.gt = 0, (tagger.tags) = "form:\"limit\""]; // limit size per page
string sort = 3 [(tagger.tags) = "form:\"sort\""]; // sort by column name of table, default is -{{.CrudInfo.ColumnName}}, the - sign indicates descending order.
}
message List{{.TableName}}ByLast{{.CrudInfo.ColumnNameCamel}}Reply {
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 1;
}
`
protoFileForSimpleWebCommonTmpl *template.Template
protoFileForSimpleWebCommonTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "tagger/tagger.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
/*
Reference https://github.com/grpc-ecosystem/grpc-gateway/blob/db7fbefff7c04877cdb32e16d4a248a024428207/examples/internal/proto/examplepb/a_bit_of_everything.proto
Default settings for generating swagger documents
NOTE: because json does not support 64 bits, the int64 and uint64 types under *.swagger.json are automatically converted to string types
Tips: add swagger option to rpc method, example:
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "get user by id",
description: "get user by id",
security: {
security_requirement: {
key: "BearerAuth";
value: {}
}
}
};
*/
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
host: "localhost:8080"
base_path: ""
info: {
title: "serverNameExample api docs";
version: "2.0";
}
schemes: HTTP;
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
security_definitions: {
security: {
key: "BearerAuth";
value: {
type: TYPE_API_KEY;
in: IN_HEADER;
name: "Authorization";
description: "Type Bearer your-jwt-token to Value";
}
}
}
};
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}"
body: "*"
};
}
// delete {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc DeleteBy{{.CrudInfo.ColumnNameCamel}}(Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {
option (google.api.http) = {
delete: "/api/v1/{{.TName}}/left_curly_bracket{{.CrudInfo.ColumnNameCamelFCL}}right_curly_bracket"
};
}
// update {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc UpdateBy{{.CrudInfo.ColumnNameCamel}}(Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {
option (google.api.http) = {
put: "/api/v1/{{.TName}}/left_curly_bracket{{.CrudInfo.ColumnNameCamelFCL}}right_curly_bracket"
body: "*"
};
}
// get {{.TName}} by {{.CrudInfo.ColumnNameCamelFCL}}
rpc GetBy{{.CrudInfo.ColumnNameCamel}}(Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request) returns (Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply) {
option (google.api.http) = {
get: "/api/v1/{{.TName}}/left_curly_bracket{{.CrudInfo.ColumnNameCamelFCL}}right_curly_bracket"
};
}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/list"
body: "*"
};
}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
If used to generate code that supports the HTTP protocol, notes for defining message fields:
1. If the route contains the path parameter, such as /api/v1/userExample/{id}, the defined
message must contain the name of the path parameter and the name should be added
with a new tag, such as int64 id = 1 [(tagger.tags) = "uri:\"id\""];
2. If the request url is followed by a query parameter, such as /api/v1/getUserExample?name=Tom,
a form tag must be added when defining the query parameter in the message, such as:
string name = 1 [(tagger.tags) = "form:\"name\""].
3. If the message field name contain underscores(such as 'field_name'), it will cause a problem
where the JSON field names of the Swagger request parameters are different from those of the
GRPC JSON tag names. There are two solutions: Solution 1, remove the underline from the
message field name. Option 2, use the tool 'protoc-go-inject-tag' to modify the JSON tag name,
such as: string first_name = 1 ; // @gotags: json:"firstName"
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1;
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetWebProtoValidation}};
}
message Delete{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageUpdateCode
message Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
}
// protoMessageDetailCode
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{.CrudInfo.ProtoType}} {{.CrudInfo.ColumnNameCamelFCL}} = 1 {{.CrudInfo.GetWebProtoValidation}};
}
message Get{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Reply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.CrudInfo.TableNamePluralCamelFCL}} = 2;
}
`
protoMessageCreateCommonTmpl *template.Template
protoMessageCreateCommonTmplRaw = `message Create{{.TableName}}Request {
{{- range $i, $v := .Fields}}
{{$v.GoType}} {{$v.JSONName}} = {{$v.AddOne $i}}; {{if $v.Comment}} // {{$v.Comment}}{{end}}
{{- end}}
}`
protoMessageUpdateCommonTmpl *template.Template
protoMessageUpdateCommonTmplRaw = `message Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request {
{{- range $i, $v := .Fields}}
{{$v.GoType}} {{$v.JSONName}} = {{$v.AddOneWithTag2 $i}}; {{if $v.Comment}} // {{$v.Comment}}{{end}}
{{- end}}
}`
protoMessageDetailCommonTmpl *template.Template
protoMessageDetailCommonTmplRaw = `message {{.TableName}} {
{{- range $i, $v := .Fields}}
{{$v.GoType}} {{$v.JSONName}} = {{$v.AddOne $i}}; {{if $v.Comment}} // {{$v.Comment}}{{end}}
{{- end}}
}`
serviceStructCommonTmpl *template.Template
serviceStructCommonTmplRaw = `
{
name: "Create",
fn: func() (interface{}, error) {
// todo enter parameters before testing
// serviceCreateStructCode
},
wantErr: false,
},
{
name: "UpdateBy{{.CrudInfo.ColumnNameCamel}}",
fn: func() (interface{}, error) {
// todo enter parameters before testing
// serviceUpdateStructCode
},
wantErr: false,
},
`
serviceCreateStructCommonTmpl *template.Template
serviceCreateStructCommonTmplRaw = ` req := &serverNameExampleV1.Create{{.TableName}}Request{
{{- range .Fields}}
{{.Name}}: {{.GoTypeZero}}, {{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
return cli.Create(ctx, req)`
serviceUpdateStructCommonTmpl *template.Template
serviceUpdateStructCommonTmplRaw = ` req := &serverNameExampleV1.Update{{.TableName}}By{{.CrudInfo.ColumnNameCamel}}Request{
{{- range .Fields}}
{{.Name}}: {{.GoTypeZero}}, {{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
return cli.UpdateBy{{.CrudInfo.ColumnNameCamel}}(ctx, req)`
commonTmplParseOnce sync.Once
)
func initCommonTemplate() {
commonTmplParseOnce.Do(func() {
var err, errSum error
handlerCreateStructCommonTmpl, err = template.New("goPostStruct").Parse(handlerCreateStructCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "handlerCreateStructCommonTmplRaw:"+err.Error())
}
handlerUpdateStructCommonTmpl, err = template.New("goPutStruct").Parse(handlerUpdateStructCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "handlerUpdateStructCommonTmplRaw:"+err.Error())
}
handlerDetailStructCommonTmpl, err = template.New("goGetStruct").Parse(handlerDetailStructCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "handlerDetailStructCommonTmplRaw:"+err.Error())
}
protoFileCommonTmpl, err = template.New("protoFile").Parse(protoFileCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileCommonTmplRaw:"+err.Error())
}
protoFileSimpleCommonTmpl, err = template.New("protoFileSimple").Parse(protoFileSimpleCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileSimpleCommonTmplRaw:"+err.Error())
}
protoFileForWebCommonTmpl, err = template.New("protoFileForWeb").Parse(protoFileForWebCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileForWebCommonTmplRaw:"+err.Error())
}
protoFileForSimpleWebCommonTmpl, err = template.New("protoFileForSimpleWeb").Parse(protoFileForSimpleWebCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileForSimpleWebCommonTmplRaw:"+err.Error())
}
protoMessageCreateCommonTmpl, err = template.New("protoMessageCreate").Parse(protoMessageCreateCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoMessageCreateCommonTmplRaw:"+err.Error())
}
protoMessageUpdateCommonTmpl, err = template.New("protoMessageUpdate").Parse(protoMessageUpdateCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoMessageUpdateCommonTmplRaw:"+err.Error())
}
protoMessageDetailCommonTmpl, err = template.New("protoMessageDetail").Parse(protoMessageDetailCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoMessageDetailCommonTmplRaw:"+err.Error())
}
serviceCreateStructCommonTmpl, err = template.New("serviceCreateStruct").Parse(serviceCreateStructCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "serviceCreateStructCommonTmplRaw:"+err.Error())
}
serviceUpdateStructCommonTmpl, err = template.New("serviceUpdateStruct").Parse(serviceUpdateStructCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "serviceUpdateStructCommonTmplRaw:"+err.Error())
}
serviceStructCommonTmpl, err = template.New("serviceStruct").Parse(serviceStructCommonTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "serviceStructCommonTmplRaw:"+err.Error())
}
if errSum != nil {
panic(errSum)
}
})
}
package parser
import (
"context"
"fmt"
"strings"
"sync/atomic"
"time"
"github.com/huandu/xstrings"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/mongo"
mgoOptions "go.mongodb.org/mongo-driver/mongo/options"
"gitlab.wanzhuangkj.com/tush/xpkg/mgo"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
const (
goTypeOID = "primitive.ObjectID"
goTypeInt = "int"
goTypeInt64 = "int64"
goTypeFloat64 = "float64"
goTypeString = "string"
goTypeTime = "time.Time"
goTypeBool = "bool"
goTypeNil = "nil"
goTypeBytes = "[]byte"
goTypeStrings = "[]string"
goTypeInts = "[]int"
goTypeInterface = "interface{}"
goTypeSliceInterface = "[]interface{}"
// SubStructKey sub struct key
SubStructKey = "_sub_struct_"
// ProtoSubStructKey proto sub struct key
ProtoSubStructKey = "_proto_sub_struct_"
oidName = "_id"
)
var mgoTypeToGo = map[bsontype.Type]string{
bson.TypeObjectID: goTypeOID,
bson.TypeInt32: goTypeInt,
bson.TypeInt64: goTypeInt64,
bson.TypeDouble: goTypeFloat64,
bson.TypeString: goTypeString,
bson.TypeArray: goTypeSliceInterface,
bson.TypeEmbeddedDocument: goTypeInterface,
bson.TypeTimestamp: goTypeTime,
bson.TypeDateTime: goTypeTime,
bson.TypeBoolean: goTypeBool,
bson.TypeNull: goTypeNil,
bson.TypeBinary: goTypeBytes,
bson.TypeUndefined: goTypeInterface,
bson.TypeCodeWithScope: goTypeString,
bson.TypeSymbol: goTypeString,
bson.TypeRegex: goTypeString,
bson.TypeDecimal128: goTypeInterface,
bson.TypeDBPointer: goTypeInterface,
bson.TypeMinKey: goTypeInt,
bson.TypeMaxKey: goTypeInt,
bson.TypeJavaScript: goTypeString,
}
var jsonTagFormat int32 = 1 // 0: snake case, 1: camel case
// SetJSONTagSnakeCase set json tag format to snake case
func SetJSONTagSnakeCase() {
atomic.AddInt32(&jsonTagFormat, -jsonTagFormat)
}
// SetJSONTagCamelCase set json tag format to camel case
func SetJSONTagCamelCase() {
atomic.AddInt32(&jsonTagFormat, 1)
}
// MgoField mongo field
type MgoField struct {
Name string `json:"name"`
Type string `json:"type"`
Comment string `json:"comment"`
ObjectStr string `json:"objectStr"`
ProtoObjectStr string `json:"protoObjectStr"`
}
// GetMongodbTableInfo get table info from mongodb
func GetMongodbTableInfo(dsn string, tableName string) ([]*MgoField, error) {
timeout := time.Second * 5
opts := &mgoOptions.ClientOptions{Timeout: &timeout}
dsn = utils.AdaptiveMongodbDsn(dsn)
db, err := mgo.Init(dsn, opts)
if err != nil {
return nil, err
}
return getMongodbTableFields(db, tableName)
}
func getMongodbTableFields(db *mongo.Database, collectionName string) ([]*MgoField, error) {
findOpts := new(mgoOptions.FindOneOptions)
findOpts.Sort = bson.M{oidName: -1}
result := db.Collection(collectionName).FindOne(context.Background(), bson.M{}, findOpts)
raw, err := result.Raw()
if err != nil {
return nil, err
}
elements, err := raw.Elements()
if err != nil {
return nil, err
}
fields := []*MgoField{}
names := []string{}
for _, element := range elements {
name := element.Key()
if name == "deleted_at" { // filter deleted_at, used for soft delete
continue
}
names = append(names, name)
t, o, p := getTypeFromMgo(name, element)
fields = append(fields, &MgoField{
Name: name,
Type: t,
ObjectStr: o,
ProtoObjectStr: p,
})
}
return embedTimeField(names, fields), nil
}
func getTypeFromMgo(name string, element bson.RawElement) (goTypeStr string, goObjectStr string, protoObjectStr string) {
v := element.Value()
switch v.Type {
case bson.TypeEmbeddedDocument:
var br bson.Raw = v.Value
es, err := br.Elements()
if err != nil {
return goTypeInterface, "", ""
}
return parseObject(name, es)
case bson.TypeArray:
var br bson.Raw = v.Value
es, err := br.Elements()
if err != nil {
return goTypeInterface, "", ""
}
if len(es) == 0 {
return goTypeInterface, "", ""
}
t, o, p := parseArray(name, es[0])
return convertToSingular(t, o, p)
}
if goType, ok := mgoTypeToGo[v.Type]; !ok {
return goTypeInterface, "", ""
} else { //nolint
return goType, "", ""
}
}
func parseObject(name string, elements []bson.RawElement) (goTypeStr string, goObjectStr string, protoObjectStr string) {
var goObjStr string
var protoObjStr string
for num, element := range elements {
t, _, _ := getTypeFromMgo(name, element)
k := element.Key()
var jsonTag string
if jsonTagFormat == 0 {
jsonTag = xstrings.ToSnakeCase(k)
} else {
jsonTag = toLowerFirst(xstrings.ToCamelCase(k))
}
goObjStr += fmt.Sprintf(" %s %s `bson:\"%s\" json:\"%s\"`\n", xstrings.ToCamelCase(k), t, k, jsonTag)
num++
protoObjStr += fmt.Sprintf(" %s %s = %d;\n", convertToProtoFieldType(name, t), k, num)
}
return "*" + xstrings.ToCamelCase(name),
fmt.Sprintf("type %s struct {\n%s}\n", xstrings.ToCamelCase(name), goObjStr),
fmt.Sprintf("message %s {\n%s}\n", xstrings.ToCamelCase(name), protoObjStr)
}
func parseArray(name string, element bson.RawElement) (goTypeStr string, goObjectStr string, protoObjectStr string) {
t, o, p := getTypeFromMgo(name, element)
if o != "" {
return "[]" + t, o, p
}
return "[]" + t, "", ""
}
func toLowerFirst(str string) string {
if len(str) == 0 {
return str
}
return strings.ToLower(string(str[0])) + str[1:]
}
func embedTimeField(names []string, fields []*MgoField) []*MgoField {
isHaveCreatedAt, isHaveUpdatedAt := false, false
for _, name := range names {
if name == "created_at" || name == "createdAt" {
isHaveCreatedAt = true
}
if name == "updated_at" || name == "updatedAt" {
isHaveUpdatedAt = true
}
names = append(names, name)
}
var timeFields []*MgoField
if !isHaveCreatedAt {
timeFields = append(timeFields, &MgoField{
Name: "created_at",
Type: goTypeTime,
})
}
if !isHaveUpdatedAt {
timeFields = append(timeFields, &MgoField{
Name: "updated_at",
Type: goTypeTime,
})
}
if len(timeFields) == 0 {
return fields
}
return append(fields, timeFields...)
}
// ConvertToSQLByMgoFields convert to mysql table ddl
func ConvertToSQLByMgoFields(tableName string, fields []*MgoField) (string, map[string]string) {
isHaveID := false
fieldStr := ""
srcMongoTypeMap := make(map[string]string) // name:type
objectStrs := []string{}
protoObjectStrs := []string{}
for _, field := range fields {
switch field.Type {
case goTypeInterface, goTypeSliceInterface:
srcMongoTypeMap[field.Name] = xstrings.ToCamelCase(field.Name)
default:
srcMongoTypeMap[field.Name] = field.Type
}
if field.Name == oidName {
isHaveID = true
srcMongoTypeMap["id"] = field.Type
continue
}
fieldStr += fmt.Sprintf(" `%s` %s,\n", field.Name, convertMongoToMysqlType(field.Type))
if field.ObjectStr != "" {
objectStrs = append(objectStrs, field.ObjectStr)
protoObjectStrs = append(protoObjectStrs, field.ProtoObjectStr)
}
}
fieldStr = strings.TrimSuffix(fieldStr, ",\n")
if isHaveID {
fieldStr = " `id` varchar(24),\n" + fieldStr + ",\n PRIMARY KEY (id)"
}
if len(objectStrs) > 0 {
srcMongoTypeMap[SubStructKey] = strings.Join(objectStrs, "\n") + "\n"
srcMongoTypeMap[ProtoSubStructKey] = strings.Join(protoObjectStrs, "\n") + "\n"
}
return fmt.Sprintf("CREATE TABLE `%s` (\n%s\n);", tableName, fieldStr), srcMongoTypeMap
}
// nolint
func convertMongoToMysqlType(goType string) string {
switch goType {
case goTypeInt:
return "int"
case goTypeInt64:
return "bigint"
case goTypeFloat64:
return "double"
case goTypeString:
return "varchar(255)"
case goTypeTime:
return "timestamp" //nolint
case goTypeBool:
return "tinyint(1)"
case goTypeOID, goTypeNil, goTypeBytes, goTypeInterface, goTypeSliceInterface, goTypeInts, goTypeStrings:
return "json"
}
return "json"
}
// nolint
func convertToProtoFieldType(name string, goType string) string {
switch goType {
case "int":
return "int32"
case "uint":
return "uint32" //nolint
case "time.Time":
return "int64"
case "float32":
return "float"
case "float64":
return "double"
case goTypeInts, "[]int64":
return "repeated int64"
case "[]int32":
return "repeated int32"
case "[]byte":
return "string"
case goTypeStrings:
return "repeated string"
}
if strings.Contains(goType, "[]") {
t := strings.TrimLeft(goType, "[]")
if strings.Contains(name, t) {
return "repeated " + t
}
}
return goType
}
// MgoFieldToGoStruct convert to go struct
func MgoFieldToGoStruct(name string, fs []*MgoField) string {
var str = ""
var objStr string
for _, f := range fs {
if f.Name == oidName {
str += " ID primitive.ObjectID `bson:\"_id\" json:\"id\"`\n"
continue
}
if f.Type == goTypeInterface || f.Type == goTypeSliceInterface {
f.Type = xstrings.ToCamelCase(f.Name)
}
str += fmt.Sprintf(" %s %s `bson:\"%s\" json:\"%s\"`\n", xstrings.ToCamelCase(f.Name), f.Type, f.Name, f.Name)
if f.ObjectStr != "" {
objStr += f.ObjectStr + "\n"
}
}
return fmt.Sprintf("type %s struct {\n%s}\n\n%s\n", xstrings.ToCamelCase(name), str, objStr)
}
func toSingular(word string) string {
if strings.HasSuffix(word, "es") {
if len(word) > 2 {
return word[:len(word)-2]
}
} else if strings.HasSuffix(word, "s") {
if len(word) > 1 {
return word[:len(word)-1]
}
}
return word
}
func nameToSingular(goTypeStr string, targetObjectStr string, markStr string) string {
name := strings.ReplaceAll(goTypeStr, "[]*", "")
prefixStr := markStr + " " + name
l := len(prefixStr)
if len(targetObjectStr) <= l {
return targetObjectStr
}
if prefixStr == targetObjectStr[:l] {
targetObjectStr = toSingular(prefixStr) + " " + targetObjectStr[l:]
return targetObjectStr
}
return targetObjectStr
}
func convertToSingular(goTypeStr string, objectStr string, protoObjectStr string) (tStr string, oStr string, pObjStr string) {
if !strings.Contains(goTypeStr, "[]*") || objectStr == "" {
return goTypeStr, objectStr, protoObjectStr
}
objectStr = nameToSingular(goTypeStr, objectStr, "type")
protoObjectStr = nameToSingular(goTypeStr, protoObjectStr, "message")
goTypeStr = toSingular(goTypeStr)
return goTypeStr, objectStr, protoObjectStr
}
package parser
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" //nolint
)
// GetMysqlTableInfo get table info from mysql
func GetMysqlTableInfo(dsn, tableName string) (string, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return "", fmt.Errorf("GetMysqlTableInfo error, %v", err)
}
defer db.Close() //nolint
rows, err := db.Query("SHOW CREATE TABLE `" + tableName + "`")
if err != nil {
return "", fmt.Errorf("query show create table error, %v", err)
}
defer rows.Close() //nolint
if !rows.Next() {
return "", fmt.Errorf("not found found table '%s'", tableName)
}
var table string
var info string
err = rows.Scan(&table, &info)
if err != nil {
return "", err
}
return info, nil
}
// GetTableInfo get table info from mysql
// Deprecated: replaced by GetMysqlTableInfo
func GetTableInfo(dsn, tableName string) (string, error) {
return GetMysqlTableInfo(dsn, tableName)
}
package parser
// NullStyle null type
type NullStyle int
// nolint
const (
NullDisable NullStyle = iota
NullInSql
NullInPointer
)
// Option function
type Option func(*options)
type options struct {
DBDriver string
FieldTypes map[string]string // name:type
Charset string
Collation string
JSONTag bool
JSONNamedType int
TablePrefix string
ColumnPrefix string
NoNullType bool
NullStyle NullStyle
Package string
GormType bool
ForceTableName bool
IsEmbed bool // is gorm.Model embedded
IsWebProto bool // true: proto file include router path and swagger info, false: normal proto file without router and swagger
IsExtendedAPI bool // true: extended api (9 api), false: basic api (5 api)
IsCustomTemplate bool // true: custom extend template, false: xmall template
}
var defaultOptions = options{
DBDriver: "mysql",
FieldTypes: map[string]string{},
NullStyle: NullInSql,
Package: "model",
}
// WithDBDriver set db driver
func WithDBDriver(driver string) Option {
return func(o *options) {
if driver != "" {
o.DBDriver = driver
}
}
}
// WithFieldTypes set field types
func WithFieldTypes(fieldTypes map[string]string) Option {
return func(o *options) {
if fieldTypes != nil {
o.FieldTypes = fieldTypes
}
}
}
// WithCharset set charset
func WithCharset(charset string) Option {
return func(o *options) {
o.Charset = charset
}
}
// WithCollation set collation
func WithCollation(collation string) Option {
return func(o *options) {
o.Collation = collation
}
}
// WithTablePrefix set table prefix
func WithTablePrefix(p string) Option {
return func(o *options) {
o.TablePrefix = p
}
}
// WithColumnPrefix set column prefix
func WithColumnPrefix(p string) Option {
return func(o *options) {
o.ColumnPrefix = p
}
}
// WithJSONTag set json tag, 0 for underscore, other values for hump
func WithJSONTag(namedType int) Option {
return func(o *options) {
o.JSONTag = true
o.JSONNamedType = namedType
}
}
// WithNoNullType set NoNullType
func WithNoNullType() Option {
return func(o *options) {
o.NoNullType = true
}
}
// WithNullStyle set NullType
func WithNullStyle(s NullStyle) Option {
return func(o *options) {
o.NullStyle = s
}
}
// WithPackage set package name
func WithPackage(pkg string) Option {
return func(o *options) {
o.Package = pkg
}
}
// WithGormType will write type in gorm tag
func WithGormType() Option {
return func(o *options) {
o.GormType = true
}
}
// WithForceTableName set forceFloats
func WithForceTableName() Option {
return func(o *options) {
o.ForceTableName = true
}
}
// WithEmbed is embed gorm.Model
func WithEmbed() Option {
return func(o *options) {
o.IsEmbed = true
}
}
// WithWebProto set proto file type
func WithWebProto() Option {
return func(o *options) {
o.IsWebProto = true
}
}
// WithExtendedAPI set extended api
func WithExtendedAPI() Option {
return func(o *options) {
o.IsExtendedAPI = true
}
}
// WithCustomTemplate set custom template
func WithCustomTemplate() Option {
return func(o *options) {
o.IsCustomTemplate = true
}
}
func parseOption(options []Option) options {
o := defaultOptions
for _, f := range options {
f(&o)
}
if o.NoNullType {
o.NullStyle = NullDisable
}
return o
}
// Package parser is a library that parses to go structures based on sql
// and generates the code needed based on the template.
package parser
import (
"bufio"
"bytes"
"errors"
"fmt"
"go/format"
"sort"
"strings"
"text/template"
"github.com/huandu/xstrings"
"github.com/jinzhu/inflection"
"github.com/zhufuyi/sqlparser/ast"
"github.com/zhufuyi/sqlparser/dependency/mysql"
"github.com/zhufuyi/sqlparser/dependency/types"
"github.com/zhufuyi/sqlparser/parser"
)
const (
// TableName table name
TableName = "__table_name__"
// CodeTypeModel model code
CodeTypeModel = "model"
// CodeTypeJSON json code
CodeTypeJSON = "json"
// CodeTypeDAO update fields code
CodeTypeDAO = "dao"
// CodeTypeHandler handler request and respond code
CodeTypeHandler = "handler"
// CodeTypeProto proto file code
CodeTypeProto = "proto"
// CodeTypeService grpc service code
CodeTypeService = "service"
// CodeTypeCrudInfo crud info json data
CodeTypeCrudInfo = "crud_info"
// CodeTypeTableInfo table info json data
CodeTypeTableInfo = "table_info"
// DBDriverMysql mysql driver
DBDriverMysql = "mysql"
// DBDriverPostgresql postgresql driver
DBDriverPostgresql = "postgresql"
// DBDriverTidb tidb driver
DBDriverTidb = "tidb"
// DBDriverSqlite sqlite driver
DBDriverSqlite = "sqlite"
// DBDriverMongodb mongodb driver
DBDriverMongodb = "mongodb"
jsonTypeName = "datatypes.JSON"
jsonPkgPath = "gorm.io/datatypes"
)
// Codes content
type Codes struct {
Model []string // model code
UpdateFields []string // update fields code
ModelJSON []string // model json code
HandlerStruct []string // handler request and respond code
}
// modelCodes model code
type modelCodes struct {
Package string
ImportPath []string
StructCode []string
}
// ParseSQL generate different usage codes based on sql
func ParseSQL(sql string, options ...Option) (map[string]string, error) {
initTemplate()
initCommonTemplate()
opt := parseOption(options)
stmts, err := parser.New().Parse(sql, opt.Charset, opt.Collation)
if err != nil {
return nil, err
}
modelStructCodes := make([]string, 0, len(stmts))
updateFieldsCodes := make([]string, 0, len(stmts))
handlerStructCodes := make([]string, 0, len(stmts))
protoFileCodes := make([]string, 0, len(stmts))
serviceStructCodes := make([]string, 0, len(stmts))
modelJSONCodes := make([]string, 0, len(stmts))
importPath := make(map[string]struct{})
tableNames := make([]string, 0, len(stmts))
primaryKeysCodes := make([]string, 0, len(stmts))
tableInfoCodes := make([]string, 0, len(stmts))
for _, stmt := range stmts {
if ct, ok := stmt.(*ast.CreateTableStmt); ok {
code, err2 := makeCode(ct, opt)
if err2 != nil {
return nil, err2
}
modelStructCodes = append(modelStructCodes, code.modelStruct)
updateFieldsCodes = append(updateFieldsCodes, code.updateFields)
handlerStructCodes = append(handlerStructCodes, code.handlerStruct)
protoFileCodes = append(protoFileCodes, code.protoFile)
serviceStructCodes = append(serviceStructCodes, code.serviceStruct)
modelJSONCodes = append(modelJSONCodes, code.modelJSON)
tableNames = append(tableNames, toCamel(ct.Table.Name.String()))
primaryKeysCodes = append(primaryKeysCodes, code.crudInfo)
tableInfoCodes = append(tableInfoCodes, string(code.tableInfo))
for _, s := range code.importPaths {
importPath[s] = struct{}{}
}
}
}
importPathArr := make([]string, 0, len(importPath))
for s := range importPath {
importPathArr = append(importPathArr, s)
}
sort.Strings(importPathArr)
mc := modelCodes{
Package: opt.Package,
ImportPath: importPathArr,
StructCode: modelStructCodes,
}
modelCode, err := getModelCode(mc)
if err != nil {
return nil, err
}
var codesMap = map[string]string{
CodeTypeModel: modelCode,
CodeTypeJSON: strings.Join(modelJSONCodes, "\n\n"),
CodeTypeDAO: strings.Join(updateFieldsCodes, "\n\n"),
CodeTypeHandler: strings.Join(handlerStructCodes, "\n\n"),
CodeTypeProto: strings.Join(protoFileCodes, "\n\n"),
CodeTypeService: strings.Join(serviceStructCodes, "\n\n"),
TableName: strings.Join(tableNames, ", "),
CodeTypeCrudInfo: strings.Join(primaryKeysCodes, "||||"),
CodeTypeTableInfo: strings.Join(tableInfoCodes, "||||"),
}
return codesMap, nil
}
type tmplData struct {
TableNamePrefix string
RawTableName string // raw table name, example: foo_bar
TableName string // table name in camel case, example: FooBar
TName string // table name first letter in lower case, example: fooBar
NameFunc bool
Fields []tmplField
Comment string
SubStructs string // sub structs for model
ProtoSubStructs string // sub structs for protobuf
DBDriver string
CrudInfo *CrudInfo
}
type tmplField struct {
IsPrimaryKey bool // is primary key
ColName string // table column name
Name string // convert to camel case
GoType string // convert to go type
Tag string
Comment string
JSONName string
DBDriver string
rewriterField *rewriterField
}
type rewriterField struct {
goType string
path string
}
func (d tmplData) isCommonStyle(isEmbed bool) bool {
if d.DBDriver != DBDriverMongodb && !isEmbed && !d.CrudInfo.isIDPrimaryKey() {
return true
}
return false
}
// ConditionZero type of condition 0, used in dao template code
func (t tmplField) ConditionZero() string {
switch t.GoType {
case "int8", "int16", "int32", "int64", "int", "uint8", "uint16", "uint32", "uint64", "uint", "float64", "float32", //nolint
"sql.NullInt32", "sql.NullInt64", "sql.NullFloat64": //nolint
return `!= 0`
case "string", "sql.NullString": //nolint
return `!= ""`
case "time.Time", "*time.Time", "sql.NullTime": //nolint
return `.IsZero() == false`
case "[]byte", "[]string", "[]int", "interface{}": //nolint
return `!= nil` //nolint
case "bool": //nolint
return `!= false /*Warning: if the value itself is false, can't be updated*/`
}
if t.DBDriver == DBDriverMongodb {
if t.GoType == goTypeOID {
return `!= primitive.NilObjectID`
}
if t.GoType == "*"+t.Name {
return `!= nil`
}
if strings.Contains(t.GoType, "[]") {
return `!= nil`
}
}
return `!= ` + t.GoType
}
// GoZero type of 0, used in model to json template code
func (t tmplField) GoZero() string {
switch t.GoType {
case "int8", "int16", "int32", "int64", "int", "uint8", "uint16", "uint32", "uint64", "uint", "float64", "float32",
"sql.NullInt32", "sql.NullInt64", "sql.NullFloat64":
return `= 0`
case "string", "sql.NullString":
return `= "string"`
case "time.Time", "*time.Time", "sql.NullTime":
return `= "0000-01-00T00:00:00.000+08:00"`
case "[]byte", "[]string", "[]int", "interface{}": //nolint
return `= nil` //nolint
case "bool": //nolint
return `= false`
}
if t.DBDriver == DBDriverMongodb {
if t.GoType == goTypeOID {
return `= primitive.NilObjectID`
}
if t.GoType == "*"+t.Name {
return `= nil`
}
if strings.Contains(t.GoType, "[]") {
return `= nil`
}
}
return `= ` + t.GoType
}
// GoTypeZero type of 0, used in service template code
func (t tmplField) GoTypeZero() string {
switch t.GoType {
case "int8", "int16", "int32", "int64", "int", "uint8", "uint16", "uint32", "uint64", "uint", "float64", "float32",
"sql.NullInt32", "sql.NullInt64", "sql.NullFloat64":
return `0`
case "string", "sql.NullString", jsonTypeName:
return `""`
case "time.Time", "*time.Time", "sql.NullTime":
return `0 /*time.Now().Second()*/`
case "[]byte", "[]string", "[]int", "interface{}": //nolint
return `nil` //nolint
case "bool": //nolint
return `false`
}
if t.DBDriver == DBDriverMongodb {
if t.GoType == goTypeOID {
return `primitive.NilObjectID`
}
if t.GoType == "*"+t.Name {
return `nil` //nolint
}
if strings.Contains(t.GoType, "[]") {
return `nil` //nolint
}
}
return t.GoType
}
// AddOne counter
func (t tmplField) AddOne(i int) int {
return i + 1
}
// AddOneWithTag counter and add id tag
func (t tmplField) AddOneWithTag(i int) string {
if t.ColName == "id" {
if t.DBDriver == DBDriverMongodb {
return fmt.Sprintf(`%d [(validate.rules).string.min_len = 6, (tagger.tags) = "uri:\"id\""]`, i+1)
}
return fmt.Sprintf(`%d [(validate.rules).%s.gt = 0, (tagger.tags) = "uri:\"id\""]`, i+1, t.GoType)
}
return fmt.Sprintf("%d", i+1)
}
func (t tmplField) AddOneWithTag2(i int) string {
if t.IsPrimaryKey || t.ColName == "id" {
if t.GoType == "string" {
return fmt.Sprintf(`%d [(validate.rules).string.min_len = 1, (tagger.tags) = "uri:\"%s\""]`, i+1, t.JSONName)
}
return fmt.Sprintf(`%d [(validate.rules).%s.gt = 0, (tagger.tags) = "uri:\"%s\""]`, i+1, t.GoType, t.JSONName)
}
return fmt.Sprintf("%d", i+1)
}
func getProtoFieldName(fields []tmplField) string {
for _, field := range fields {
if field.IsPrimaryKey || field.ColName == "id" {
return field.JSONName
}
}
return ""
}
const (
__mysqlModel__ = "__mysqlModel__" //nolint
__type__ = "__type__" //nolint
)
var replaceFields = map[string]string{
__mysqlModel__: "sgorm.Model",
__type__: "",
}
const (
columnID = "id"
_columnID = "_id"
columnCreatedAt = "created_at"
columnUpdatedAt = "updated_at"
columnDeletedAt = "deleted_at"
columnMysqlModel = __mysqlModel__
)
var ignoreColumns = map[string]struct{}{
columnID: {},
columnCreatedAt: {},
columnUpdatedAt: {},
columnDeletedAt: {},
columnMysqlModel: {},
}
func isIgnoreFields(colName string, falseColumn ...string) bool {
for _, v := range falseColumn {
if colName == v {
return false
}
}
_, ok := ignoreColumns[colName]
return ok
}
type codeText struct {
importPaths []string
modelStruct string
modelJSON string
updateFields string
handlerStruct string
protoFile string
serviceStruct string
crudInfo string
tableInfo []byte
}
// nolint
func makeCode(stmt *ast.CreateTableStmt, opt options) (*codeText, error) {
importPath := make([]string, 0, 1)
data := tmplData{
TableNamePrefix: opt.TablePrefix,
RawTableName: stmt.Table.Name.String(),
DBDriver: opt.DBDriver,
}
tablePrefix := data.TableNamePrefix
if tablePrefix != "" && strings.HasPrefix(data.RawTableName, tablePrefix) {
data.NameFunc = true
data.TableName = toCamel(data.RawTableName[len(tablePrefix):])
} else {
data.TableName = toCamel(data.RawTableName)
}
data.TName = firstLetterToLower(data.TableName)
if opt.ForceTableName || data.RawTableName != inflection.Plural(data.RawTableName) {
data.NameFunc = true
}
switch opt.DBDriver {
case DBDriverMongodb:
if opt.JSONNamedType != 0 {
SetJSONTagCamelCase()
} else {
SetJSONTagSnakeCase()
}
}
// find table comment
for _, o := range stmt.Options {
if o.Tp == ast.TableOptionComment {
data.Comment = o.StrValue
break
}
}
isPrimaryKey := make(map[string]bool)
for _, con := range stmt.Constraints {
if con.Tp == ast.ConstraintPrimaryKey {
isPrimaryKey[con.Keys[0].Column.String()] = true
}
if con.Tp == ast.ConstraintForeignKey {
// TODO: foreign key support
}
}
columnPrefix := opt.ColumnPrefix
for _, col := range stmt.Cols {
colName := col.Name.Name.String()
goFieldName := colName
if columnPrefix != "" && strings.HasPrefix(goFieldName, columnPrefix) {
goFieldName = goFieldName[len(columnPrefix):]
}
jsonName := colName
if opt.JSONNamedType == 0 { // snake case
jsonName = customToSnake(jsonName)
} else {
jsonName = customToCamel(jsonName) // camel case (default)
}
field := tmplField{
Name: toCamel(goFieldName),
ColName: colName,
JSONName: jsonName,
}
tags := make([]string, 0, 4)
// make GORM's tag
gormTag := strings.Builder{}
gormTag.WriteString("column:")
gormTag.WriteString(colName)
if opt.GormType {
gormTag.WriteString(";type:")
switch opt.DBDriver {
case DBDriverMysql, DBDriverTidb, DBDriverSqlite:
gormTag.WriteString(col.Tp.InfoSchemaStr())
case DBDriverPostgresql:
gormTag.WriteString(opt.FieldTypes[colName])
}
}
if isPrimaryKey[colName] {
field.IsPrimaryKey = true
gormTag.WriteString(";primary_key")
}
isNotNull := false
canNull := false
for _, o := range col.Options {
switch o.Tp {
case ast.ColumnOptionPrimaryKey:
if !isPrimaryKey[colName] {
gormTag.WriteString(";primary_key")
isPrimaryKey[colName] = true
}
case ast.ColumnOptionNotNull:
isNotNull = true
case ast.ColumnOptionAutoIncrement:
gormTag.WriteString(";AUTO_INCREMENT")
case ast.ColumnOptionDefaultValue:
if value := getDefaultValue(o.Expr); value != "" {
gormTag.WriteString(";default:")
gormTag.WriteString(value)
}
case ast.ColumnOptionUniqKey:
gormTag.WriteString(";unique")
case ast.ColumnOptionNull:
//gormTag.WriteString(";NULL")
canNull = true
case ast.ColumnOptionOnUpdate: // For Timestamp and Datetime only.
case ast.ColumnOptionFulltext:
case ast.ColumnOptionComment:
field.Comment = o.Expr.GetDatum().GetString()
default:
//return "", nil, errors.Errorf(" unsupport option %d\n", o.Tp)
}
}
field.DBDriver = opt.DBDriver
switch opt.DBDriver {
case DBDriverMongodb: // mongodb
tags = append(tags, "bson", gormTag.String())
if opt.JSONTag {
if strings.ToLower(jsonName) == "_id" {
jsonName = "id"
}
field.JSONName = jsonName
tags = append(tags, "json", jsonName)
}
field.Tag = makeTagStr(tags)
field.GoType = opt.FieldTypes[colName]
if field.GoType == "time.Time" {
importPath = append(importPath, "time")
}
default: // gorm
if !isPrimaryKey[colName] && isNotNull {
gormTag.WriteString(";not null")
}
tags = append(tags, "gorm", gormTag.String())
if opt.JSONTag {
tags = append(tags, "json", jsonName)
}
field.Tag = makeTagStr(tags)
// get type in golang
nullStyle := opt.NullStyle
if !canNull {
nullStyle = NullDisable
}
goType, pkg, rrField := mysqlToGoType(col.Tp, nullStyle)
if pkg != "" {
importPath = append(importPath, pkg)
}
field.GoType = goType
field.rewriterField = rrField
if opt.DBDriver == DBDriverPostgresql {
if opt.FieldTypes[colName] == "bool" {
field.GoType = "bool" // rewritten type
}
}
}
data.Fields = append(data.Fields, field)
}
if v, ok := opt.FieldTypes[SubStructKey]; ok {
data.SubStructs = v
}
if v, ok := opt.FieldTypes[ProtoSubStructKey]; ok {
data.ProtoSubStructs = v
}
if len(data.Fields) == 0 {
return nil, errors.New("no columns found in table " + data.TableName)
}
data.CrudInfo = newCrudInfo(data)
data.CrudInfo.IsCommonType = data.isCommonStyle(opt.IsEmbed)
if opt.IsCustomTemplate {
tableInfo := newTableInfo(data)
return &codeText{tableInfo: tableInfo.getCode()}, nil
}
updateFieldsCode, err := getUpdateFieldsCode(data, opt.IsEmbed)
if err != nil {
return nil, err
}
modelStructCode, importPaths, err := getModelStructCode(data, importPath, opt.IsEmbed, opt.JSONNamedType)
if err != nil {
return nil, err
}
modelJSONCode, err := getModelJSONCode(data)
if err != nil {
return nil, err
}
handlerStructCode := ""
serviceStructCode := ""
protoFileCode := ""
if data.isCommonStyle(opt.IsEmbed) {
handlerStructCode, err = getCommonHandlerStructCodes(data, opt.JSONNamedType)
if err != nil {
return nil, err
}
serviceStructCode, err = getCommonServiceStructCode(data)
if err != nil {
return nil, err
}
protoFileCode, err = getCommonProtoFileCode(data, opt.JSONNamedType, opt.IsWebProto, opt.IsExtendedAPI)
if err != nil {
return nil, err
}
} else {
handlerStructCode, err = getHandlerStructCodes(data, opt.JSONNamedType)
if err != nil {
return nil, err
}
serviceStructCode, err = getServiceStructCode(data)
if err != nil {
return nil, err
}
protoFileCode, err = getProtoFileCode(data, opt.JSONNamedType, opt.IsWebProto, opt.IsExtendedAPI)
if err != nil {
return nil, err
}
}
return &codeText{
importPaths: importPaths,
modelStruct: modelStructCode,
modelJSON: modelJSONCode,
updateFields: updateFieldsCode,
handlerStruct: handlerStructCode,
protoFile: protoFileCode,
serviceStruct: serviceStructCode,
crudInfo: data.CrudInfo.getCode(),
}, nil
}
// nolint
func getModelStructCode(data tmplData, importPaths []string, isEmbed bool, jsonNamedType int) (string, []string, error) {
// filter to ignore field fields
var newFields = []tmplField{}
var newImportPaths = []string{}
if isEmbed {
newFields = append(newFields, tmplField{
Name: __mysqlModel__,
ColName: __mysqlModel__,
GoType: __type__,
Tag: `gorm:"embedded"`,
Comment: "embed id and time\n",
})
isHaveTimeType := false
for _, field := range data.Fields {
if isIgnoreFields(field.ColName) {
continue
}
switch field.DBDriver {
case DBDriverMysql, DBDriverTidb, DBDriverPostgresql:
if field.rewriterField != nil {
if field.rewriterField.goType == jsonTypeName {
field.GoType = jsonTypeName
importPaths = append(importPaths, jsonPkgPath)
}
}
}
newFields = append(newFields, field)
if strings.Contains(field.GoType, "time.Time") {
isHaveTimeType = true
}
}
data.Fields = newFields
// filter time package name
if isHaveTimeType {
newImportPaths = importPaths
} else {
for _, path := range importPaths {
if path == "time" { //nolint
continue
}
newImportPaths = append(newImportPaths, path)
}
}
newImportPaths = append(newImportPaths, "gitlab.wanzhuangkj.com/tush/xpkg/sgorm")
} else {
for i, field := range data.Fields {
switch field.DBDriver {
case DBDriverMongodb:
if field.Name == "ID" {
data.Fields[i].GoType = goTypeOID
importPaths = append(importPaths, "go.mongodb.org/mongo-driver/bson/primitive")
}
default:
if strings.Contains(field.GoType, "time.Time") {
data.Fields[i].GoType = "*time.Time"
continue
}
// force conversion of ID field to uint64 type
if field.Name == "ID" {
data.Fields[i].GoType = "uint64"
if data.isCommonStyle(isEmbed) {
data.Fields[i].GoType = data.CrudInfo.GoType
}
}
switch field.DBDriver {
case DBDriverMysql, DBDriverTidb, DBDriverPostgresql:
if field.rewriterField != nil {
if field.rewriterField.goType == jsonTypeName {
data.Fields[i].GoType = jsonTypeName
importPaths = append(importPaths, jsonPkgPath)
}
}
}
}
}
newImportPaths = importPaths
}
builder := strings.Builder{}
err := modelStructTmpl.Execute(&builder, data)
if err != nil {
return "", nil, fmt.Errorf("modelStructTmpl.Execute error: %v", err)
}
code, err := format.Source([]byte(builder.String()))
if err != nil {
return "", nil, fmt.Errorf("modelStructTmpl format.Source error: %v", err)
}
structCode := string(code)
// restore the real embedded fields
if isEmbed {
gormEmbed := replaceFields[__mysqlModel__]
if jsonNamedType == 0 { // snake case
gormEmbed += "2" // sgorm.Model2
}
structCode = strings.ReplaceAll(structCode, __mysqlModel__, gormEmbed)
structCode = strings.ReplaceAll(structCode, __type__, replaceFields[__type__])
}
if data.SubStructs != "" {
structCode += data.SubStructs
}
if data.DBDriver == DBDriverMongodb {
structCode = strings.ReplaceAll(structCode, `bson:"column:`, `bson:"`)
structCode = strings.ReplaceAll(structCode, `;type:"`, `"`)
structCode = strings.ReplaceAll(structCode, `;type:;primary_key`, ``)
structCode = strings.ReplaceAll(structCode, `bson:"id" json:"id"`, `bson:"_id" json:"id"`)
}
return structCode, newImportPaths, nil
}
func getModelCode(data modelCodes) (string, error) {
builder := strings.Builder{}
err := modelTmpl.Execute(&builder, data)
if err != nil {
return "", err
}
code, err := format.Source([]byte(builder.String()))
if err != nil {
return "", fmt.Errorf("format.Source error: %v", err)
}
return string(code), nil
}
func getUpdateFieldsCode(data tmplData, isEmbed bool) (string, error) {
_ = isEmbed
// filter fields
var newFields = []tmplField{}
for _, field := range data.Fields {
falseColumns := []string{}
if isIgnoreFields(field.ColName, falseColumns...) || field.ColName == columnID || field.ColName == _columnID {
continue
}
switch field.DBDriver {
case DBDriverMysql, DBDriverTidb, DBDriverPostgresql:
if field.rewriterField != nil {
if field.rewriterField.goType == jsonTypeName {
field.GoType = "[]byte"
}
}
}
newFields = append(newFields, field)
}
data.Fields = newFields
buf := new(bytes.Buffer)
err := updateFieldTmpl.Execute(buf, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func getHandlerStructCodes(data tmplData, jsonNamedType int) (string, error) {
newFields := []tmplField{}
for _, field := range data.Fields {
if field.DBDriver == DBDriverMongodb { // mongodb
if field.Name == "ID" {
field.GoType = "string"
}
if "*"+field.Name == field.GoType {
field.GoType = "*model." + field.Name
}
if strings.Contains(field.GoType, "[]*") {
field.GoType = "[]*model." + strings.ReplaceAll(field.GoType, "[]*", "")
}
}
if jsonNamedType == 0 { // snake case
field.JSONName = customToSnake(field.ColName)
} else {
field.JSONName = customToCamel(field.ColName) // camel case (default)
}
newFields = append(newFields, field)
}
data.Fields = newFields
postStructCode, err := tmplExecuteWithFilter(data, handlerCreateStructTmpl)
if err != nil {
return "", fmt.Errorf("handlerCreateStructTmpl error: %v", err)
}
putStructCode, err := tmplExecuteWithFilter(data, handlerUpdateStructTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handlerUpdateStructTmpl error: %v", err)
}
getStructCode, err := tmplExecuteWithFilter(data, handlerDetailStructTmpl, columnID, columnCreatedAt, columnUpdatedAt)
if err != nil {
return "", fmt.Errorf("handlerDetailStructTmpl error: %v", err)
}
return postStructCode + putStructCode + getStructCode, nil
}
// customized filter fields
func tmplExecuteWithFilter(data tmplData, tmpl *template.Template, reservedColumns ...string) (string, error) {
var newFields = []tmplField{}
for _, field := range data.Fields {
if isIgnoreFields(field.ColName, reservedColumns...) {
continue
}
if field.DBDriver == DBDriverMongodb { // mongodb
if strings.ToLower(field.Name) == "id" {
field.GoType = "string"
}
}
newFields = append(newFields, field)
}
data.Fields = newFields
builder := strings.Builder{}
err := tmpl.Execute(&builder, data)
if err != nil {
return "", fmt.Errorf("tmpl.Execute error: %v", err)
}
return builder.String(), nil
}
func getModelJSONCode(data tmplData) (string, error) {
builder := strings.Builder{}
err := modelJSONTmpl.Execute(&builder, data)
if err != nil {
return "", err
}
code, err := format.Source([]byte(builder.String()))
if err != nil {
return "", fmt.Errorf("format.Source error: %v", err)
}
modelJSONCode := strings.ReplaceAll(string(code), " =", ":")
modelJSONCode = addCommaToJSON(modelJSONCode)
return modelJSONCode, nil
}
func getProtoFileCode(data tmplData, jsonNamedType int, isWebProto bool, isExtendedAPI bool) (string, error) {
data.Fields = goTypeToProto(data.Fields, jsonNamedType, false)
var err error
builder := strings.Builder{}
if isWebProto {
if isExtendedAPI {
err = protoFileForWebTmpl.Execute(&builder, data)
} else {
err = protoFileForSimpleWebTmpl.Execute(&builder, data)
}
if err != nil {
return "", err
}
} else {
if isExtendedAPI {
err = protoFileTmpl.Execute(&builder, data)
} else {
err = protoFileSimpleTmpl.Execute(&builder, data)
}
if err != nil {
return "", err
}
}
code := builder.String()
protoMessageCreateCode, err := tmplExecuteWithFilter(data, protoMessageCreateTmpl)
if err != nil {
return "", fmt.Errorf("handle protoMessageCreateTmpl error: %v", err)
}
protoMessageUpdateCode, err := tmplExecuteWithFilter(data, protoMessageUpdateTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handle protoMessageUpdateTmpl error: %v", err)
}
if !isWebProto {
protoMessageUpdateCode = strings.ReplaceAll(protoMessageUpdateCode, `, (tagger.tags) = "uri:\"id\""`, "")
}
protoMessageDetailCode, err := tmplExecuteWithFilter(data, protoMessageDetailTmpl, columnID, columnCreatedAt, columnUpdatedAt)
if err != nil {
return "", fmt.Errorf("handle protoMessageDetailTmpl error: %v", err)
}
code = strings.ReplaceAll(code, "// protoMessageCreateCode", protoMessageCreateCode)
code = strings.ReplaceAll(code, "// protoMessageUpdateCode", protoMessageUpdateCode)
code = strings.ReplaceAll(code, "// protoMessageDetailCode", protoMessageDetailCode)
code = strings.ReplaceAll(code, "*time.Time", "int64")
code = strings.ReplaceAll(code, "time.Time", "int64")
code = adaptedDbType(data, isWebProto, code)
return code, nil
}
const (
createTableReplyFieldCodeMark = "// createTableReplyFieldCode"
deleteTableByIDRequestFieldCodeMark = "// deleteTableByIDRequestFieldCode"
deleteTableByIDsRequestFieldCodeMark = "// deleteTableByIDsRequestFieldCode"
getTableByIDRequestFieldCodeMark = "// getTableByIDRequestFieldCode"
getTableByIDsRequestFieldCodeMark = "// getTableByIDsRequestFieldCode"
listTableByLastIDRequestFieldCodeMark = "// listTableByLastIDRequestFieldCode"
)
var grpcDefaultProtoMessageFieldCodes = map[string]string{
createTableReplyFieldCodeMark: "uint64 id = 1;",
deleteTableByIDRequestFieldCodeMark: "uint64 id = 1 [(validate.rules).uint64.gt = 0];",
deleteTableByIDsRequestFieldCodeMark: "repeated uint64 ids = 1 [(validate.rules).repeated.min_items = 1];",
getTableByIDRequestFieldCodeMark: "uint64 id = 1 [(validate.rules).uint64.gt = 0];",
getTableByIDsRequestFieldCodeMark: "repeated uint64 ids = 1 [(validate.rules).repeated.min_items = 1];",
listTableByLastIDRequestFieldCodeMark: "uint64 lastID = 1; // last id",
}
var webDefaultProtoMessageFieldCodes = map[string]string{
createTableReplyFieldCodeMark: "uint64 id = 1;",
deleteTableByIDRequestFieldCodeMark: `uint64 id =1 [(validate.rules).uint64.gt = 0, (tagger.tags) = "uri:\"id\""];`,
deleteTableByIDsRequestFieldCodeMark: "repeated uint64 ids = 1 [(validate.rules).repeated.min_items = 1];",
getTableByIDRequestFieldCodeMark: `uint64 id =1 [(validate.rules).uint64.gt = 0, (tagger.tags) = "uri:\"id\"" ];`,
getTableByIDsRequestFieldCodeMark: "repeated uint64 ids = 1 [(validate.rules).repeated.min_items = 1];",
listTableByLastIDRequestFieldCodeMark: `uint64 lastID = 1 [(tagger.tags) = "form:\"lastID\""]; // last id`,
}
var grpcProtoMessageFieldCodes = map[string]string{
createTableReplyFieldCodeMark: "string id = 1;",
deleteTableByIDRequestFieldCodeMark: "string id = 1 [(validate.rules).string.min_len = 6];",
deleteTableByIDsRequestFieldCodeMark: "repeated string ids = 1 [(validate.rules).repeated.min_items = 1];",
getTableByIDRequestFieldCodeMark: "string id = 1 [(validate.rules).string.min_len = 6];",
getTableByIDsRequestFieldCodeMark: "repeated string ids = 1 [(validate.rules).repeated.min_items = 1];",
listTableByLastIDRequestFieldCodeMark: "string lastID = 1; // last id",
}
var webProtoMessageFieldCodes = map[string]string{
createTableReplyFieldCodeMark: "string id = 1;",
deleteTableByIDRequestFieldCodeMark: `string id =1 [(validate.rules).string.min_len = 6, (tagger.tags) = "uri:\"id\""];`,
deleteTableByIDsRequestFieldCodeMark: "repeated string ids = 1 [(validate.rules).repeated.min_items = 1];",
getTableByIDRequestFieldCodeMark: `string id =1 [(validate.rules).string.min_len = 6, (tagger.tags) = "uri:\"id\"" ];`,
getTableByIDsRequestFieldCodeMark: "repeated string ids = 1 [(validate.rules).repeated.min_items = 1];",
listTableByLastIDRequestFieldCodeMark: `string lastID = 1 [(tagger.tags) = "form:\"lastID\""]; // last id`,
}
func adaptedDbType(data tmplData, isWebProto bool, code string) string {
switch data.DBDriver {
case DBDriverMongodb: // mongodb
if isWebProto {
code = replaceProtoMessageFieldCode(code, webProtoMessageFieldCodes)
} else {
code = replaceProtoMessageFieldCode(code, grpcProtoMessageFieldCodes)
}
default:
if isWebProto {
code = replaceProtoMessageFieldCode(code, webDefaultProtoMessageFieldCodes)
} else {
code = replaceProtoMessageFieldCode(code, grpcDefaultProtoMessageFieldCodes)
}
}
if data.ProtoSubStructs != "" {
code += "\n" + data.ProtoSubStructs
}
return code
}
func replaceProtoMessageFieldCode(code string, messageFields map[string]string) string {
for k, v := range messageFields {
code = strings.ReplaceAll(code, k, v)
}
return code
}
func getServiceStructCode(data tmplData) (string, error) {
builder := strings.Builder{}
err := serviceStructTmpl.Execute(&builder, data)
if err != nil {
return "", err
}
code := builder.String()
serviceCreateStructCode, err := tmplExecuteWithFilter(data, serviceCreateStructTmpl)
if err != nil {
return "", fmt.Errorf("handle serviceCreateStructTmpl error: %v", err)
}
serviceCreateStructCode = strings.ReplaceAll(serviceCreateStructCode, "ID:", "Id:")
serviceUpdateStructCode, err := tmplExecuteWithFilter(data, serviceUpdateStructTmpl, columnID)
if err != nil {
return "", fmt.Errorf("handle serviceUpdateStructTmpl error: %v", err)
}
serviceUpdateStructCode = strings.ReplaceAll(serviceUpdateStructCode, "ID:", "Id:")
code = strings.ReplaceAll(code, "// serviceCreateStructCode", serviceCreateStructCode)
code = strings.ReplaceAll(code, "// serviceUpdateStructCode", serviceUpdateStructCode)
return code, nil
}
func addCommaToJSON(modelJSONCode string) string {
r := strings.NewReader(modelJSONCode)
buf := bufio.NewReader(r)
lines := []string{}
count := 0
for {
line, err := buf.ReadString(byte('\n'))
if err != nil {
break
}
lines = append(lines, line)
if len(line) > 5 {
count++
}
}
out := ""
for _, line := range lines {
if len(line) < 5 && (strings.Contains(line, "{") || strings.Contains(line, "}")) {
out += line
continue
}
count--
if count == 0 {
out += line
continue
}
index := bytes.IndexByte([]byte(line), '\n')
out += line[:index] + "," + line[index:]
}
return out
}
// nolint
func mysqlToGoType(colTp *types.FieldType, style NullStyle) (name string, path string, rrField *rewriterField) {
if style == NullInSql {
path = "database/sql"
switch colTp.Tp {
case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong:
name = "sql.NullInt32"
case mysql.TypeLonglong:
name = "sql.NullInt64"
case mysql.TypeFloat, mysql.TypeDouble:
name = "sql.NullFloat64"
case mysql.TypeString, mysql.TypeVarchar, mysql.TypeVarString,
mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob:
name = "sql.NullString"
case mysql.TypeTimestamp, mysql.TypeDatetime, mysql.TypeDate:
name = "sql.NullTime"
case mysql.TypeDecimal, mysql.TypeNewDecimal:
name = "sql.NullString"
case mysql.TypeJSON, mysql.TypeEnum:
name = "sql.NullString"
default:
return "UnSupport", "", nil
}
} else {
switch colTp.Tp {
case mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24, mysql.TypeLong:
if mysql.HasUnsignedFlag(colTp.Flag) {
name = "uint"
} else {
name = "int"
}
case mysql.TypeLonglong:
if mysql.HasUnsignedFlag(colTp.Flag) {
name = "uint64"
} else {
name = "int64"
}
case mysql.TypeFloat, mysql.TypeDouble:
name = "float64"
case mysql.TypeString, mysql.TypeVarchar, mysql.TypeVarString,
mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob:
name = "string"
case mysql.TypeTimestamp, mysql.TypeDatetime, mysql.TypeDate:
path = "time" //nolint
name = "time.Time"
case mysql.TypeDecimal, mysql.TypeNewDecimal:
name = "string"
case mysql.TypeEnum:
name = "string"
case mysql.TypeJSON:
name = "string"
rrField = &rewriterField{
goType: jsonTypeName,
path: jsonPkgPath,
}
default:
return "UnSupport", "", nil
}
if style == NullInPointer {
name = "*" + name
}
}
return name, path, rrField
}
// nolint
func goTypeToProto(fields []tmplField, jsonNameType int, isCommonStyle bool) []tmplField {
var newFields []tmplField
for _, field := range fields {
switch field.GoType {
case "int":
field.GoType = "int32"
case "uint":
field.GoType = "uint32"
case "time.Time", "*time.Time":
field.GoType = "string"
case "float32":
field.GoType = "float"
case "float64":
field.GoType = "double"
case goTypeInts, "[]int64":
field.GoType = "repeated int64"
case "[]int32":
field.GoType = "repeated int32"
case "[]byte":
field.GoType = "string"
case goTypeStrings:
field.GoType = "repeated string"
case jsonTypeName:
field.GoType = "string"
}
if field.DBDriver == DBDriverMongodb && field.GoType != "" {
if field.GoType[0] == '*' {
field.GoType = field.GoType[1:]
} else if strings.Contains(field.GoType, "[]*") {
field.GoType = "repeated " + strings.ReplaceAll(field.GoType, "[]*", "")
}
if field.GoType == "[]time.Time" {
field.GoType = "repeated string"
}
} else {
if strings.ToLower(field.Name) == "id" && !isCommonStyle {
field.GoType = "uint64"
}
}
if jsonNameType == 0 { // snake case
field.JSONName = customToSnake(field.ColName)
} else {
field.JSONName = customToCamel(field.ColName) // camel case (default)
}
newFields = append(newFields, field)
}
return newFields
}
func makeTagStr(tags []string) string {
builder := strings.Builder{}
for i := 0; i < len(tags)/2; i++ {
builder.WriteString(tags[i*2])
builder.WriteString(`:"`)
builder.WriteString(tags[i*2+1])
builder.WriteString(`" `)
}
if builder.Len() > 0 {
return builder.String()[:builder.Len()-1]
}
return builder.String()
}
func getDefaultValue(expr ast.ExprNode) (value string) {
if expr.GetDatum().Kind() != types.KindNull {
value = fmt.Sprintf("%v", expr.GetDatum().GetValue())
} else if expr.GetFlag() != ast.FlagConstant {
if expr.GetFlag() == ast.FlagHasFunc {
if funcExpr, ok := expr.(*ast.FuncCallExpr); ok {
value = funcExpr.FnName.O
}
}
}
return value
}
var acronym = map[string]struct{}{
"ID": {},
"IP": {},
}
// nolint
func toCamel(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return s
}
s += "."
n := strings.Builder{}
n.Grow(len(s))
temp := strings.Builder{}
temp.Grow(len(s))
wordFirst := true
for _, v := range []byte(s) {
vIsCap := v >= 'A' && v <= 'Z'
vIsLow := v >= 'a' && v <= 'z'
if wordFirst && vIsLow {
v -= 'a' - 'A'
}
if vIsCap || vIsLow {
temp.WriteByte(v)
wordFirst = false
} else {
isNum := v >= '0' && v <= '9'
wordFirst = isNum || v == '_' || v == ' ' || v == '-' || v == '.'
if temp.Len() > 0 && wordFirst {
word := temp.String()
upper := strings.ToUpper(word)
if _, ok := acronym[upper]; ok {
n.WriteString(upper)
} else {
n.WriteString(word)
}
temp.Reset()
}
if isNum {
n.WriteByte(v)
}
}
}
str := n.String()
if len(str) > 2 {
if str[len(str)-2:] == "Id" {
str = str[:len(str)-2] + "ID"
} else if str[len(str)-2:] == "Ip" {
str = str[:len(str)-2] + "IP"
}
}
return str
}
func firstLetterToLower(str string) string {
if len(str) == 0 {
return str
}
if (str[0] >= 'A' && str[0] <= 'Z') || (str[0] >= 'a' && str[0] <= 'z') {
return strings.ToLower(str[:1]) + str[1:]
}
return str
}
func customToCamel(str string) string {
str = firstLetterToLower(toCamel(str))
if len(str) == 2 {
if str == "iD" {
str = "id"
} else if str == "iP" {
str = "ip"
}
}
return str
}
func customToSnake(str string) string {
if len(str) == 0 {
return str
}
index := 0
for _, c := range str {
if c != '_' {
break
}
index++
}
if index != 0 {
str = str[index:]
}
if len(str) == 2 {
if str == "iD" {
str = "id"
} else if str == "iP" {
str = "ip"
}
}
return xstrings.ToSnakeCase(str)
}
package parser
import (
"fmt"
"testing"
"github.com/jinzhu/inflection"
"github.com/stretchr/testify/assert"
"github.com/zhufuyi/sqlparser/dependency/mysql"
"github.com/zhufuyi/sqlparser/dependency/types"
)
func TestParseSQL(t *testing.T) {
sqls := []string{`create table user (
id bigint unsigned auto_increment,
created_at datetime null,
updated_at datetime null,
deleted_at datetime null,
name char(50) not null comment '用户名',
password char(100) not null comment '密码',
email char(50) not null comment '邮件',
phone bigint unsigned not null comment '手机号码',
age tinyint not null comment '年龄',
gender tinyint not null comment '性别,1:男,2:女,3:未知',
status tinyint not null comment '账号状态,1:未激活,2:已激活,3:封禁',
login_state tinyint not null comment '登录状态,1:未登录,2:已登录',
primary key (id),
constraint user_email_uindex
unique (email)
);`,
`create table user_order (
id varchar(36) not null comment '订单id',
product_id varchar(36) not null comment '商品id',
user_id bigint unsigned not null comment '用户id',
status smallint null comment '0:未支付, 1:已支付, 2:已取消',
created_at timestamp null comment '创建时间',
updated_at timestamp null comment '更新时间',
primary key (id)
);`,
`create table user_str (
user_id varchar(36) not null comment '用户id',
username varchar(50) not null comment '用户名',
email varchar(100) not null comment '邮箱',
created_at datetime null comment '创建时间',
primary key (user_id),
constraint email
unique (email)
);`,
`create table user_no_primary (
username varchar(50) not null comment '用户名',
email varchar(100) not null comment '邮箱',
user_id varchar(36) not null comment '用户id',
created_at datetime null comment '创建时间',
constraint email
unique (email)
);`}
for _, sql := range sqls {
codes, err := ParseSQL(sql, WithJSONTag(0), WithEmbed())
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
codes, err = ParseSQL(sql, WithJSONTag(1), WithWebProto(), WithDBDriver(DBDriverMysql))
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
codes, err = ParseSQL(sql, WithJSONTag(0), WithDBDriver(DBDriverPostgresql))
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
codes, err = ParseSQL(sql, WithJSONTag(0), WithDBDriver(DBDriverSqlite))
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
codes, err = ParseSQL(sql, WithDBDriver(DBDriverSqlite), WithCustomTemplate())
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
break
}
}
//printCode(codes)
}
}
func TestParseSqlWithTablePrefix(t *testing.T) {
sql := `CREATE TABLE t_person_info (
id BIGINT(11) AUTO_INCREMENT NOT NULL COMMENT 'id',
age INT(11) unsigned NULL,
name VARCHAR(30) NOT NULL DEFAULT 'default_name' COMMENT 'name',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
login_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
gender INT(8) NULL,
num INT(11) DEFAULT 3 NULL,
comment TEXT,
PRIMARY KEY (id)
) COMMENT="person info";`
codes, err := ParseSQL(sql, WithTablePrefix("t_"), WithJSONTag(0), WithNullStyle(NullDisable))
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
codes, err = ParseSQL(sql, WithTablePrefix("t_"), WithJSONTag(0), WithCustomTemplate())
assert.Nil(t, err)
for k, v := range codes {
if k != CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
jsonData := codes[CodeTypeTableInfo]
t.Log(jsonData)
t.Log(UnMarshalTableInfo(jsonData))
codes, err = ParseSQL(sql, WithTablePrefix("t_"), WithJSONTag(0), WithEmbed())
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
codes, err = ParseSQL(sql, WithTablePrefix("t_"), WithJSONTag(0), WithWebProto())
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
codes, err = ParseSQL(sql, WithTablePrefix("t_"), WithJSONTag(0), WithDBDriver(DBDriverPostgresql))
assert.Nil(t, err)
for k, v := range codes {
if k == CodeTypeTableInfo {
continue
}
assert.NotEmpty(t, k)
assert.NotEmpty(t, v)
}
//printCode(codes)
}
var testData = [][]string{
{
"CREATE TABLE information (age INT(11) NULL);",
"Age int `gorm:\"column:age\"`", "",
},
{
"CREATE TABLE information (age BIGINT(11) NULL COMMENT 'is age');",
"Age int64 `gorm:\"column:age\"` // is age", "",
},
{
"CREATE TABLE information (id BIGINT(11) PRIMARY KEY AUTO_INCREMENT);",
"ID int64 `gorm:\"column:id;primary_key;AUTO_INCREMENT\"`", "",
},
{
"CREATE TABLE information (user_ip varchar(20));",
"UserIP string `gorm:\"column:user_ip\"`", "",
},
{
"CREATE TABLE information (created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP);",
"CreatedAt time.Time `gorm:\"column:created_at;default:CURRENT_TIMESTAMP;NOT NULL\"`", "time",
},
{
"CREATE TABLE information (num INT(11) DEFAULT 3 NULL);",
"Num int `gorm:\"column:num;default:3\"`", "",
},
{
"CREATE TABLE information (num double(5,6) DEFAULT 31.50 NULL);",
"Num float64 `gorm:\"column:num;default:31.50\"`", "",
},
{
"CREATE TABLE information (comment TEXT);",
"Comment string `gorm:\"column:comment\"`", "",
},
{
"CREATE TABLE information (comment TINYTEXT);",
"Comment string `gorm:\"column:comment\"`", "",
},
{
"CREATE TABLE information (comment LONGTEXT);",
"Comment string `gorm:\"column:comment\"`", "",
},
}
func TestParseSQLs(t *testing.T) {
for i, test := range testData {
msg := fmt.Sprintf("sql-%d", i)
codes, err := ParseSQL(test[0], WithNoNullType())
if !assert.NoError(t, err, msg) {
continue
}
for k, v := range codes {
if len(v) > 100 {
v = v[:100]
}
t.Log(i+1, k, v)
}
}
}
func TestConvertNames(t *testing.T) {
names := []string{"_id", "id", "iD", "user_id", "productId", "orderID", "user_name", "ip", "iP", "host_ip", "myIP"}
var convertNames []string
var convertNames2 []string
var convertNames3 []string
for _, name := range names {
convertNames = append(convertNames, toCamel(name))
convertNames2 = append(convertNames2, customToCamel(name))
convertNames3 = append(convertNames3, customToSnake(name))
}
t.Log("source: ", names)
t.Log("toCamel: ", convertNames)
t.Log("customToCamel:", convertNames2)
t.Log("customToSnake:", convertNames3)
}
func Test_parseOption(t *testing.T) {
opts := []Option{
WithDBDriver("foo"),
WithFieldTypes(map[string]string{"foo": "bar"}),
WithCharset("foo"),
WithCollation("foo"),
WithTablePrefix("foo"),
WithColumnPrefix("foo"),
WithJSONTag(1),
WithNoNullType(),
WithNullStyle(1),
WithPackage("model"),
WithGormType(),
WithForceTableName(),
WithEmbed(),
}
o := parseOption(opts)
assert.NotNil(t, o)
}
func Test_mysqlToGoType(t *testing.T) {
fields := []*types.FieldType{
{Tp: uint8('n')},
{Tp: mysql.TypeTiny},
{Tp: mysql.TypeLonglong},
{Tp: mysql.TypeFloat},
{Tp: mysql.TypeString},
{Tp: mysql.TypeTimestamp},
{Tp: mysql.TypeDecimal},
{Tp: mysql.TypeJSON},
}
var names []string
for _, d := range fields {
name1, _, _ := mysqlToGoType(d, NullInSql)
name2, _, _ := mysqlToGoType(d, NullInPointer)
names = append(names, name1, name2)
}
t.Log(names)
}
func Test_goTypeToProto(t *testing.T) {
fields := []tmplField{
{GoType: "int"},
{GoType: "uint"},
{GoType: "time.Time"},
}
v := goTypeToProto(fields, 1, false)
assert.NotNil(t, v)
}
func Test_initTemplate(t *testing.T) {
initTemplate()
defer func() { recover() }()
modelStructTmplRaw = "{{if .foo}}"
modelTmplRaw = "{{if .foo}}"
updateFieldTmplRaw = "{{if .foo}}"
handlerCreateStructTmplRaw = "{{if .foo}}"
handlerUpdateStructTmplRaw = "{{if .foo}}"
handlerDetailStructTmplRaw = "{{if .foo}}"
modelJSONTmplRaw = "{{if .foo}}"
protoFileTmplRaw = "{{if .foo}}"
protoFileSimpleTmplRaw = "{{if .foo}}"
protoFileForWebTmplRaw = "{{if .foo}}"
protoFileForSimpleWebTmplRaw = "{{if .foo}}"
protoMessageCreateTmplRaw = "{{if .foo}}"
protoMessageUpdateTmplRaw = "{{if .foo}}"
protoMessageDetailTmplRaw = "{{if .foo}}"
serviceCreateStructTmplRaw = "{{if .foo}}"
serviceUpdateStructTmplRaw = "{{if .foo}}"
serviceStructTmplRaw = "{{if .foo}}"
initTemplate()
}
func TestGetMysqlTableInfo(t *testing.T) {
info, err := GetMysqlTableInfo("root:123456@(192.168.3.37:3306)/account", "user_order")
t.Log(err, info)
}
func TestGetPostgresqlTableInfo(t *testing.T) {
var (
dbname = "account"
tableName = "user_order"
dsn = fmt.Sprintf("host=192.168.3.37 port=5432 user=root password=123456 dbname=%s sslmode=disable", dbname)
)
fields, err := GetPostgresqlTableInfo(dsn, tableName)
if err != nil {
t.Log(err)
return
}
printPGFields(fields)
sql, fieldTypes := ConvertToSQLByPgFields(tableName, fields)
t.Log(sql)
t.Log(fieldTypes)
}
func Test_getPostgresqlTableFields(t *testing.T) {
defer func() { _ = recover() }()
_, _ = getPostgresqlTableFields(nil, "foobar")
}
func TestGetSqliteTableInfo(t *testing.T) {
info, err := GetSqliteTableInfo("..\\..\\..\\test\\sql\\sqlite\\xmall.db", "user_order")
t.Log(err, info)
}
func TestGetMongodbTableInfo(t *testing.T) {
var (
dbname = "account"
tableName = "people"
dsn = fmt.Sprintf("mongodb://root:123456@192.168.3.37:27017/%s", dbname)
)
fields, err := GetMongodbTableInfo(dsn, tableName)
if err != nil {
t.Log(err)
return
}
sql, fieldTypes := ConvertToSQLByMgoFields(tableName, fields)
t.Log(sql)
t.Log(fieldTypes)
}
func TestConvertToSQLByPgFields(t *testing.T) {
fields := []*PGField{
{Name: "id", Type: "smallint"},
{Name: "name", Type: "character", Lengthvar: 24, Notnull: false},
{Name: "age", Type: "smallint", Notnull: true},
}
sql, tps := ConvertToSQLByPgFields("foobar", fields)
t.Log(sql, tps)
}
func Test_PGField_getMysqlType(t *testing.T) {
fields := []*PGField{
{Type: "smallint"},
{Type: "bigint"},
{Type: "real"},
{Type: "decimal"},
{Type: "double precision"},
{Type: "money"},
{Type: "character", Lengthvar: 24},
{Type: "text"},
{Type: "timestamp"},
{Type: "date"},
{Type: "time"},
{Type: "interval"},
{Type: "boolean"},
}
for _, field := range fields {
t.Log(field.getMysqlType(), getType(field))
}
}
func Test_SqliteField_getMysqlType(t *testing.T) {
fields := []*SqliteField{
{Type: "integer"},
{Type: "text"},
{Type: "real"},
{Type: "numeric"},
{Type: "blob"},
{Type: "datetime"},
{Type: "boolean"},
{Type: "unknown_type"},
}
for _, field := range fields {
t.Log(field.getMysqlType())
}
}
func printCode(code map[string]string) {
for k, v := range code {
fmt.Printf("\n\n----------------- %s --------------------\n%s\n", k, v)
}
}
func printPGFields(fields []*PGField) {
fmt.Printf("%-20v %-20v %-20v %-20v %-20v %-20v %-20v\n", "Name", "Type", "Length", "Lengthvar", "Notnull", "Comment", "IsPrimaryKey")
for _, p := range fields {
fmt.Printf("%-20v %-20v %-20v %-20v %-20v %-20v %-20v\n", p.Name, p.Type, p.Length, p.Lengthvar, p.Notnull, p.Comment, p.IsPrimaryKey)
}
}
func Test_getMongodbTableFields(t *testing.T) {
fields := []*MgoField{
{
Name: "_id",
Type: "primitive.ObjectID",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "age",
Type: "int",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "birthday",
Type: "time.Time",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "home_address",
Type: "HomeAddress",
ObjectStr: "type HomeAddress struct { Street string `bson:\"street\" json:\"street\"`; City string `bson:\"city\" json:\"city\"`; State string `bson:\"state\" json:\"state\"`; Zip int `bson:\"zip\" json:\"zip\"` } ",
ProtoObjectStr: `message HomeAddress {
string street = 1;
string city = 2;
string state = 3;
int32 zip = 4;
}
`,
},
{
Name: "interests",
Type: "[]string",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "is_child",
Type: "bool",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "name",
Type: "string",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "numbers",
Type: "[]int",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "shop_addresses",
Type: "[]ShopAddress",
ObjectStr: "type ShopAddress struct { CityO string `bson:\"city_o\" json:\"cityO\"`; StateO string `bson:\"state_o\" json:\"stateO\"` }",
ProtoObjectStr: `message ShopAddress {
string city_o = 1;
string state_o = 2;
}
`,
},
{
Name: "created_at",
Type: "time.Time",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "updated_at",
Type: "time.Time",
ObjectStr: "",
ProtoObjectStr: "",
},
{
Name: "deleted_at",
Type: "*time.Time",
ObjectStr: "",
ProtoObjectStr: "",
},
}
SetJSONTagCamelCase()
goStructs := MgoFieldToGoStruct("foobar", fields)
t.Log(goStructs)
sql, fieldsMap := ConvertToSQLByMgoFields("foobar", fields)
t.Log(sql)
opts := []Option{
WithDBDriver(DBDriverMongodb),
WithFieldTypes(fieldsMap),
WithJSONTag(1),
}
codes, err := ParseSQL(sql, opts...)
if err != nil {
t.Error(err)
return
}
_ = codes
//printCode(codes)
SetJSONTagSnakeCase()
sql, fieldsMap = ConvertToSQLByMgoFields("foobar", fields)
t.Log(sql)
opts = []Option{
WithDBDriver(DBDriverMongodb),
WithFieldTypes(fieldsMap),
WithJSONTag(1),
WithWebProto(),
WithExtendedAPI(),
}
codes, err = ParseSQL(sql, opts...)
if err != nil {
t.Error(err)
return
}
//printCode(codes)
}
func Test_toSingular(t *testing.T) {
strs := []string{
"users",
"address",
"addresses",
}
for _, str := range strs {
t.Log(str, toSingular(str))
}
}
func Test_embedTimeFields(t *testing.T) {
names := []string{"age"}
fields := embedTimeField(names, []*MgoField{})
t.Log(fields)
names = []string{
"created_at",
"updated_at",
"deleted_at",
}
fields = embedTimeField(names, []*MgoField{})
t.Log(fields)
}
func TestCrudInfo(t *testing.T) {
data := tmplData{
TableName: "User",
TName: "user",
NameFunc: false,
RawTableName: "user",
Fields: []tmplField{
{
ColName: "name",
Name: "Name",
GoType: "string",
Tag: "json:\"name\"",
Comment: "姓名",
JSONName: "name",
DBDriver: "mysql",
},
{
ColName: "age",
Name: "Age",
GoType: "int",
Tag: "json:\"age\"",
Comment: "年龄",
JSONName: "age",
DBDriver: "mysql",
},
{
ColName: "created_at",
Name: "CreatedAt",
GoType: "time.Time",
Tag: "json:\"created_at\"",
Comment: "创建时间",
JSONName: "createdAt",
DBDriver: "mysql",
},
},
Comment: "用户信息",
SubStructs: "",
ProtoSubStructs: "",
DBDriver: "mysql",
}
info := newCrudInfo(data)
isPrimary := info.isIDPrimaryKey()
assert.Equal(t, false, isPrimary)
code := info.getCode()
assert.Contains(t, code, `"tableNameCamel":"User","tableNameCamelFCL":"user"`)
grpcValidation := info.GetGRPCProtoValidation()
assert.Contains(t, grpcValidation, "validate.rules")
webValidation := info.GetWebProtoValidation()
assert.Contains(t, webValidation, "validate.rules")
info = nil
_ = info.isIDPrimaryKey()
_ = info.getCode()
_ = info.GetGRPCProtoValidation()
_ = info.GetWebProtoValidation()
}
func Test_customEndOfLetterToLower(t *testing.T) {
names := []string{
"ID",
"IP",
"userID",
"orderID",
"LocalIP",
"bus",
"BUS",
"x",
"s",
}
for _, name := range names {
t.Log(customEndOfLetterToLower(name, inflection.Plural(name)))
}
}
package parser
import (
"fmt"
"strings"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// GetPostgresqlTableInfo get table info from postgres
func GetPostgresqlTableInfo(dsn string, tableName string) (PGFields, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("GetPostgresqlTableInfo error: %v", err)
}
defer closeDB(db)
return getPostgresqlTableFields(db, tableName)
}
// ConvertToSQLByPgFields convert to mysql table ddl
func ConvertToSQLByPgFields(tableName string, fields PGFields) (string, map[string]string) {
fieldStr := ""
pgTypeMap := make(map[string]string) // name:type
if len(fields) == 0 {
return "", pgTypeMap
}
for _, field := range fields {
pgTypeMap[field.Name] = getType(field)
sqlType := field.getMysqlType()
notnullStr := "not null"
if !field.Notnull {
notnullStr = "null"
}
fieldStr += fmt.Sprintf(" `%s` %s %s comment '%s',\n", field.Name, sqlType, notnullStr, field.Comment)
}
primaryField := fields.getPrimaryField()
if primaryField != nil {
fieldStr += fmt.Sprintf(" PRIMARY KEY (`%s`)\n", primaryField.Name)
} else {
fieldStr = strings.TrimSuffix(fieldStr, ",\n")
}
sqlStr := fmt.Sprintf("CREATE TABLE `%s` (\n%s\n);", tableName, fieldStr)
return sqlStr, pgTypeMap
}
// PGField postgresql field
type PGField struct {
Name string `gorm:"column:name;" json:"name"`
Type string `gorm:"column:type;" json:"type"`
Comment string `gorm:"column:comment;" json:"comment"`
Length int `gorm:"column:length;" json:"length"`
Lengthvar int `gorm:"column:lengthvar;" json:"lengthvar"`
Notnull bool `gorm:"column:notnull;" json:"notnull"`
IsPrimaryKey bool `gorm:"column:is_primary_key;" json:"is_primary_key"`
}
// nolint
func (field *PGField) getMysqlType() string {
switch field.Type {
case "smallint", "integer", "smallserial", "serial", "int2", "int4":
return "int"
case "bigint", "bigserial", "int8":
return "bigint"
case "real":
return "float"
case "decimal", "numeric", "float4", "float8":
return "decimal"
case "double precision":
return "double"
case "money":
return "varchar(30)"
case "character", "character varying", "varchar", "char", "bpchar":
if field.Lengthvar > 4 {
return fmt.Sprintf("varchar(%d)", field.Lengthvar-4)
} else {
return "varchar(100)"
}
case "text":
return "text"
case "timestamp":
return "timestamp"
case "date":
return "date"
case "time": //nolint
return "time" //nolint
case "interval":
return "year"
case "json", "jsonb":
return "json"
case "boolean", "bool":
return "bool"
case "bit":
return "tinyint(1)"
}
// unknown type convert to varchar
field.Type = "varchar(100)"
return field.Type
}
type PGFields []*PGField
func (fields PGFields) getPrimaryField() *PGField {
var f *PGField
for _, field := range fields {
if field.IsPrimaryKey || field.Name == "id" {
f = field
return f
}
}
/*
// if no primary key, find the first xxx_id field
if f == nil {
for _, field := range fields {
if strings.HasSuffix(field.Name, "_id") {
f = field
f.IsPrimaryKey = true
return f
}
}
}
// if no xxx_id field, find the first field
if f == nil {
for _, field := range fields {
f = field
f.IsPrimaryKey = true
return f
}
}
*/
return f
}
func getPostgresqlTableFields(db *gorm.DB, tableName string) (PGFields, error) {
query := fmt.Sprintf(`SELECT
a.attname AS name,
t.typname AS type,
a.attlen AS length,
a.atttypmod AS lengthvar,
a.attnotnull AS notnull,
b.description AS comment,
CASE
WHEN pk.constraint_type = 'PRIMARY KEY' THEN true
ELSE false
END AS is_primary_key
FROM pg_class c
JOIN pg_attribute a ON a.attrelid = c.oid
LEFT JOIN pg_description b ON a.attrelid = b.objoid AND a.attnum = b.objsubid
JOIN pg_type t ON a.atttypid = t.oid
LEFT JOIN (
SELECT
kcu.column_name,
con.constraint_type
FROM information_schema.table_constraints con
JOIN information_schema.key_column_usage kcu
ON con.constraint_name = kcu.constraint_name
WHERE con.constraint_type = 'PRIMARY KEY'
AND con.table_name = '%s'
) AS pk ON a.attname = pk.column_name
WHERE c.relname = '%s'
AND a.attnum > 0
ORDER BY a.attnum;`, tableName, tableName)
var fields PGFields
result := db.Raw(query).Scan(&fields)
if result.Error != nil {
return nil, fmt.Errorf("failed to get table fields: %v", result.Error)
}
return fields, nil
}
func getType(field *PGField) string {
switch field.Type {
case "character", "character varying", "varchar", "char", "bpchar":
if field.Lengthvar > 4 {
return fmt.Sprintf("varchar(%d)", field.Lengthvar-4)
}
}
return field.Type
}
func closeDB(db *gorm.DB) {
sqlDB, err := db.DB()
if err != nil {
return
}
_ = sqlDB.Close()
}
package parser
import (
"fmt"
"strings"
"gitlab.wanzhuangkj.com/tush/xpkg/sgorm/sqlite"
)
// GetSqliteTableInfo get table info from sqlite
func GetSqliteTableInfo(dbFile string, tableName string) (string, error) {
db, err := sqlite.Init(dbFile)
if err != nil {
return "", err
}
defer sqlite.Close(db) //nolint
var sqliteFields SqliteFields
sql := fmt.Sprintf("PRAGMA table_info('%s')", tableName)
err = db.Raw(sql).Scan(&sqliteFields).Error
if err != nil {
return "", err
}
return convertToSQLBySqliteFields(tableName, sqliteFields), nil
}
// SqliteField sqlite field struct
type SqliteField struct {
Cid int `gorm:"column:cid" json:"cid"`
Name string `gorm:"column:name" json:"name"`
Type string `gorm:"column:type" json:"type"`
Notnull int `gorm:"column:notnull" json:"notnull"`
DefaultValue string `gorm:"column:dflt_value" json:"dflt_value"`
Pk int `gorm:"column:pk" json:"pk"`
}
var sqliteToMysqlType = map[string]string{
"integer": "INT",
"text": "TEXT",
"real": "FLOAT",
"datetime": "DATETIME",
"blob": "BLOB",
"boolean": "TINYINT",
"numeric": " VARCHAR(255)",
"autoincrement": "auto_increment",
}
func (field *SqliteField) getMysqlType() string {
sqliteType := strings.ToLower(field.Type)
if mysqlType, ok := sqliteToMysqlType[sqliteType]; ok {
if field.Name == "id" && sqliteType == "text" {
return "VARCHAR(50)"
}
return mysqlType
}
return "VARCHAR(100)"
}
// SqliteFields sqlite fields
type SqliteFields []*SqliteField
func (fields SqliteFields) getPrimaryField() *SqliteField {
var f *SqliteField
for _, field := range fields {
if field.Pk == 1 || field.Name == "id" {
f = field
return f
}
}
/*
// if no primary key, find the first xxx_id field
if f == nil {
for _, field := range fields {
if strings.HasSuffix(field.Name, "_id") {
f = field
f.Pk = 1
return f
}
}
}
// if no xxx_id field, find the first field
if f == nil {
for _, field := range fields {
f = field
f.Pk = 1
return f
}
}
*/
return f
}
func convertToSQLBySqliteFields(tableName string, fields SqliteFields) string {
if len(fields) == 0 {
return ""
}
fieldStr := ""
for _, field := range fields {
notnullStr := "not null"
if field.Notnull == 0 {
notnullStr = "null"
}
fieldStr += fmt.Sprintf(" `%s` %s %s comment '%s',\n", field.Name, field.getMysqlType(), notnullStr, "")
}
primaryField := fields.getPrimaryField()
if primaryField != nil {
fieldStr += fmt.Sprintf(" PRIMARY KEY (`%s`)\n", primaryField.Name)
} else {
fieldStr = strings.TrimSuffix(fieldStr, ",\n")
}
return fmt.Sprintf("CREATE TABLE `%s` (\n%s\n);", tableName, fieldStr)
}
package parser
import (
"encoding/json"
"fmt"
"github.com/huandu/xstrings"
"github.com/jinzhu/inflection"
)
// TableInfo is the struct for extend template
type TableInfo struct {
TableNamePrefix string // table name prefix, example: t_
TableName string // original table name, example: foo_bar
TableNameCamel string // camel case, example: FooBar
TableNameCamelFCL string // camel case and first character lower, example: fooBar
TableNamePluralCamel string // plural, camel case, example: FooBars
TableNamePluralCamelFCL string // plural, camel case and first character lower, example: fooBars
TableNameSnake string // snake case, example: foo_bar
TableComment string // table comment
Columns []Field // columns of the table
PrimaryKey *PrimaryKey // primary key information
DBDriver string // database driver, example: mysql, postgresql, sqlite3, mongodb
ColumnSubStructure string // column sub structure for model
ColumnSubMessage string // sub message for protobuf
}
// Field is the struct for column information
type Field struct {
ColumnName string // original column name, example: foo_bar
ColumnNameCamel string // first character lower, example: FooBar
ColumnNameCamelFCL string // first character lower, example: fooBar
ColumnComment string // column comment
IsPrimaryKey bool // is primary key
GoType string // convert to go type
Tag string // tag for model struct field, default gorm tag
}
// PrimaryKey is the struct for primary key information, it used for generate CRUD code
type PrimaryKey struct {
Name string // primary key name, example: foo_bar
NameCamel string // primary key name, camel case, example: FooBar
NameCamelFCL string // primary key name, camel case and first character lower, example: fooBar
NamePluralCamel string // primary key name, plural, camel case, example: FooBars
NamePluralCamelFCL string // primary key name, plural, camel case and first character lower, example: fooBars
GoType string // go type, example: int, string
GoTypeFCU string // go type, first character upper, example: Int64, String
IsStringType bool // go type is string or not
}
func newTableInfo(data tmplData) TableInfo {
pluralName := inflection.Plural(data.TableName)
return TableInfo{
TableNamePrefix: data.TableNamePrefix,
TableName: data.RawTableName,
TableNameCamel: data.TableName,
TableNameCamelFCL: data.TName,
TableNamePluralCamel: customEndOfLetterToLower(data.TableName, pluralName),
TableNamePluralCamelFCL: customFirstLetterToLower(customEndOfLetterToLower(data.TableName, pluralName)),
TableNameSnake: xstrings.ToSnakeCase(data.TName),
TableComment: data.Comment,
Columns: getColumns(data.Fields),
PrimaryKey: getPrimaryKeyInfo(data.CrudInfo),
DBDriver: data.DBDriver,
ColumnSubStructure: data.SubStructs,
ColumnSubMessage: data.ProtoSubStructs,
}
}
func (table TableInfo) getCode() []byte {
code, err := json.Marshal(&table)
if err != nil {
fmt.Printf("table: %v, json.Marshal error: %v\n", table.TableName, err)
}
return code
}
func getColumns(fields []tmplField) []Field {
var columns []Field
for _, field := range fields {
columns = append(columns, Field{
ColumnName: field.ColName,
ColumnNameCamel: field.Name,
ColumnNameCamelFCL: customFirstLetterToLower(field.Name),
ColumnComment: field.Comment,
IsPrimaryKey: field.IsPrimaryKey,
GoType: field.GoType,
Tag: field.Tag,
})
}
return columns
}
func getPrimaryKeyInfo(info *CrudInfo) *PrimaryKey {
if info == nil {
return nil
}
return &PrimaryKey{
Name: info.ColumnName,
NameCamel: info.ColumnNameCamel,
NameCamelFCL: info.ColumnNameCamelFCL,
NamePluralCamel: info.ColumnNamePluralCamel,
NamePluralCamelFCL: info.ColumnNamePluralCamelFCL,
GoType: info.GoType,
GoTypeFCU: info.GoTypeFCU,
IsStringType: info.IsStringType,
}
}
// UnMarshalTableInfo unmarshal the json data to TableInfo struct
func UnMarshalTableInfo(data string) (map[string]interface{}, error) {
info := map[string]interface{}{}
err := json.Unmarshal([]byte(data), &info)
if err != nil {
return info, err
}
return info, nil
}
package parser
import (
"sync"
"text/template"
"github.com/pkg/errors"
)
var (
modelStructTmpl *template.Template
modelStructTmplRaw = `
{{- if .Comment -}}
// {{.TableName}} {{.Comment}}
{{end -}}
type {{.TableName}} struct {
{{- range .Fields}}
{{.Name}} {{.GoType}} {{if .Tag}}` + "`{{.Tag}}`" + `{{end}}{{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
{{if .NameFunc}}
// TableName table name
func (m *{{.TableName}}) TableName() string {
return "{{.RawTableName}}"
}
{{end}}
`
modelTmpl *template.Template
modelTmplRaw = `package {{.Package}}
{{if .ImportPath}}
import (
{{- range .ImportPath}}
"{{.}}"
{{- end}}
)
{{- end}}
{{range .StructCode}}
{{.}}
{{end}}`
updateFieldTmpl *template.Template
updateFieldTmplRaw = `
{{- range .Fields}}
if table.{{.Name}}{{.ConditionZero}} {
update["{{.ColName}}"] = table.{{.Name}}
}
{{- end}}`
handlerCreateStructTmpl *template.Template
handlerCreateStructTmplRaw = `
// Create{{.TableName}}Request request params
type Create{{.TableName}}Request struct {
{{- range .Fields}}
{{.Name}} {{.GoType}} ` + "`" + `json:"{{.JSONName}}" binding:""` + "`" + `{{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
`
handlerUpdateStructTmpl *template.Template
handlerUpdateStructTmplRaw = `
// Update{{.TableName}}ByIDRequest request params
type Update{{.TableName}}ByIDRequest struct {
{{- range .Fields}}
{{.Name}} {{.GoType}} ` + "`" + `json:"{{.JSONName}}" binding:""` + "`" + `{{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
`
handlerDetailStructTmpl *template.Template
handlerDetailStructTmplRaw = `
// {{.TableName}}ObjDetail detail
type {{.TableName}}ObjDetail struct {
{{- range .Fields}}
{{.Name}} {{.GoType}} ` + "`" + `json:"{{.JSONName}}"` + "`" + `{{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}`
modelJSONTmpl *template.Template
modelJSONTmplRaw = `{
{{- range .Fields}}
"{{.ColName}}" {{.GoZero}}
{{- end}}
}
`
protoFileTmpl *template.Template
protoFileTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {}
// delete {{.TName}} by id
rpc DeleteByID(Delete{{.TableName}}ByIDRequest) returns (Delete{{.TableName}}ByIDReply) {}
// update {{.TName}} by id
rpc UpdateByID(Update{{.TableName}}ByIDRequest) returns (Update{{.TableName}}ByIDReply) {}
// get {{.TName}} by id
rpc GetByID(Get{{.TableName}}ByIDRequest) returns (Get{{.TableName}}ByIDReply) {}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {}
// delete {{.TName}} by batch id
rpc DeleteByIDs(Delete{{.TableName}}ByIDsRequest) returns (Delete{{.TableName}}ByIDsReply) {}
// get {{.TName}} by condition
rpc GetSliceByCondition(Get{{.TableName}}ByConditionRequest) returns (Get{{.TableName}}ByConditionReply) {}
// list of {{.TName}} by batch id
rpc ListByIDs(List{{.TableName}}ByIDsRequest) returns (List{{.TableName}}ByIDsReply) {}
// list {{.TName}} by last id
rpc ListByLastID(List{{.TableName}}ByLastIDRequest) returns (List{{.TableName}}ByLastIDReply) {}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
// createTableReplyFieldCode
}
message Delete{{.TableName}}ByIDRequest {
// deleteTableByIDRequestFieldCode
}
message Delete{{.TableName}}ByIDReply {
}
// protoMessageUpdateCode
message Update{{.TableName}}ByIDReply {
}
// protoMessageDetailCode
message Get{{.TableName}}ByIDRequest {
// getTableByIDRequestFieldCode
}
message Get{{.TableName}}ByIDReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.TName}}s = 2;
}
message Delete{{.TableName}}ByIDsRequest {
// deleteTableByIDsRequestFieldCode
}
message Delete{{.TableName}}ByIDsReply {
}
message Get{{.TableName}}ByConditionRequest {
types.Conditions conditions = 1;
}
message Get{{.TableName}}ByConditionReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}ByIDsRequest {
// getTableByIDsRequestFieldCode
}
message List{{.TableName}}ByIDsReply {
repeated {{.TableName}} {{.TName}}s = 1;
}
message List{{.TableName}}ByLastIDRequest {
// listTableByLastIDRequestFieldCode
uint32 limit = 2 [(validate.rules).uint32.gt = 0]; // limit size per page
string sort = 3; // sort by column name of table, default is -id, the - sign indicates descending order.
}
message List{{.TableName}}ByLastIDReply {
repeated {{.TableName}} {{.TName}}s = 1;
}
`
protoFileSimpleTmpl *template.Template
protoFileSimpleTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {}
// delete {{.TName}} by id
rpc DeleteByID(Delete{{.TableName}}ByIDRequest) returns (Delete{{.TableName}}ByIDReply) {}
// update {{.TName}} by id
rpc UpdateByID(Update{{.TableName}}ByIDRequest) returns (Update{{.TableName}}ByIDReply) {}
// get {{.TName}} by id
rpc GetByID(Get{{.TableName}}ByIDRequest) returns (Get{{.TableName}}ByIDReply) {}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
// createTableReplyFieldCode
}
message Delete{{.TableName}}ByIDRequest {
// deleteTableByIDRequestFieldCode
}
message Delete{{.TableName}}ByIDReply {
}
// protoMessageUpdateCode
message Update{{.TableName}}ByIDReply {
}
// protoMessageDetailCode
message Get{{.TableName}}ByIDRequest {
// getTableByIDRequestFieldCode
}
message Get{{.TableName}}ByIDReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.TName}}s = 2;
}
`
protoFileForWebTmpl *template.Template
protoFileForWebTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "tagger/tagger.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
/*
Reference https://github.com/grpc-ecosystem/grpc-gateway/blob/db7fbefff7c04877cdb32e16d4a248a024428207/examples/internal/proto/examplepb/a_bit_of_everything.proto
Default settings for generating swagger documents
NOTE: because json does not support 64 bits, the int64 and uint64 types under *.swagger.json are automatically converted to string types
Tips: add swagger option to rpc method, example:
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "get user by id",
description: "get user by id",
security: {
security_requirement: {
key: "BearerAuth";
value: {}
}
}
};
*/
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
host: "localhost:8080"
base_path: ""
info: {
title: "serverNameExample api docs";
version: "2.0";
}
schemes: HTTP;
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
security_definitions: {
security: {
key: "BearerAuth";
value: {
type: TYPE_API_KEY;
in: IN_HEADER;
name: "Authorization";
description: "Type Bearer your-jwt-token to Value";
}
}
}
};
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}"
body: "*"
};
}
// delete {{.TName}} by id
rpc DeleteByID(Delete{{.TableName}}ByIDRequest) returns (Delete{{.TableName}}ByIDReply) {
option (google.api.http) = {
delete: "/api/v1/{{.TName}}/{id}"
};
}
// update {{.TName}} by id
rpc UpdateByID(Update{{.TableName}}ByIDRequest) returns (Update{{.TableName}}ByIDReply) {
option (google.api.http) = {
put: "/api/v1/{{.TName}}/{id}"
body: "*"
};
}
// get {{.TName}} by id
rpc GetByID(Get{{.TableName}}ByIDRequest) returns (Get{{.TableName}}ByIDReply) {
option (google.api.http) = {
get: "/api/v1/{{.TName}}/{id}"
};
}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/list"
body: "*"
};
}
// delete {{.TName}} by batch id
rpc DeleteByIDs(Delete{{.TableName}}ByIDsRequest) returns (Delete{{.TableName}}ByIDsReply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/delete/ids"
body: "*"
};
}
// get {{.TName}} by condition
rpc GetSliceByCondition(Get{{.TableName}}ByConditionRequest) returns (Get{{.TableName}}ByConditionReply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/condition"
body: "*"
};
}
// list of {{.TName}} by batch id
rpc ListByIDs(List{{.TableName}}ByIDsRequest) returns (List{{.TableName}}ByIDsReply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/list/ids"
body: "*"
};
}
// list {{.TName}} by last id
rpc ListByLastID(List{{.TableName}}ByLastIDRequest) returns (List{{.TableName}}ByLastIDReply) {
option (google.api.http) = {
get: "/api/v1/{{.TName}}/list"
};
}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
If used to generate code that supports the HTTP protocol, notes for defining message fields:
1. If the route contains the path parameter, such as /api/v1/userExample/{id}, the defined
message must contain the name of the path parameter and the name should be added
with a new tag, such as int64 id = 1 [(tagger.tags) = "uri:\"id\""];
2. If the request url is followed by a query parameter, such as /api/v1/getUserExample?name=Tom,
a form tag must be added when defining the query parameter in the message, such as:
string name = 1 [(tagger.tags) = "form:\"name\""].
3. If the message field name contain underscores(such as 'field_name'), it will cause a problem
where the JSON field names of the Swagger request parameters are different from those of the
GRPC JSON tag names. There are two solutions: Solution 1, remove the underline from the
message field name. Option 2, use the tool 'protoc-go-inject-tag' to modify the JSON tag name,
such as: string first_name = 1 ; // @gotags: json:"firstName"
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
// createTableReplyFieldCode
}
message Delete{{.TableName}}ByIDRequest {
// deleteTableByIDRequestFieldCode
}
message Delete{{.TableName}}ByIDReply {
}
// protoMessageUpdateCode
message Update{{.TableName}}ByIDReply {
}
// protoMessageDetailCode
message Get{{.TableName}}ByIDRequest {
// getTableByIDRequestFieldCode
}
message Get{{.TableName}}ByIDReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.TName}}s = 2;
}
message Delete{{.TableName}}ByIDsRequest {
// deleteTableByIDsRequestFieldCode
}
message Delete{{.TableName}}ByIDsReply {
}
message Get{{.TableName}}ByConditionRequest {
types.Conditions conditions = 1;
}
message Get{{.TableName}}ByConditionReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}ByIDsRequest {
// getTableByIDsRequestFieldCode
}
message List{{.TableName}}ByIDsReply {
repeated {{.TableName}} {{.TName}}s = 1;
}
message List{{.TableName}}ByLastIDRequest {
// listTableByLastIDRequestFieldCode
uint32 limit = 2 [(validate.rules).uint32.gt = 0, (tagger.tags) = "form:\"limit\""]; // limit size per page
string sort = 3 [(tagger.tags) = "form:\"sort\""]; // sort by column name of table, default is -id, the - sign indicates descending order.
}
message List{{.TableName}}ByLastIDReply {
repeated {{.TableName}} {{.TName}}s = 1;
}
`
protoFileForSimpleWebTmpl *template.Template
protoFileForSimpleWebTmplRaw = `syntax = "proto3";
package api.serverNameExample.v1;
import "api/types/types.proto";
import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "tagger/tagger.proto";
import "validate/validate.proto";
option go_package = "github.com/mooncake9527/xmall/api/serverNameExample/v1;v1";
/*
Reference https://github.com/grpc-ecosystem/grpc-gateway/blob/db7fbefff7c04877cdb32e16d4a248a024428207/examples/internal/proto/examplepb/a_bit_of_everything.proto
Default settings for generating swagger documents
NOTE: because json does not support 64 bits, the int64 and uint64 types under *.swagger.json are automatically converted to string types
Tips: add swagger option to rpc method, example:
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "get user by id",
description: "get user by id",
security: {
security_requirement: {
key: "BearerAuth";
value: {}
}
}
};
*/
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
host: "localhost:8080"
base_path: ""
info: {
title: "serverNameExample api docs";
version: "2.0";
}
schemes: HTTP;
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
security_definitions: {
security: {
key: "BearerAuth";
value: {
type: TYPE_API_KEY;
in: IN_HEADER;
name: "Authorization";
description: "Type Bearer your-jwt-token to Value";
}
}
}
};
service {{.TName}} {
// create {{.TName}}
rpc Create(Create{{.TableName}}Request) returns (Create{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}"
body: "*"
};
}
// delete {{.TName}} by id
rpc DeleteByID(Delete{{.TableName}}ByIDRequest) returns (Delete{{.TableName}}ByIDReply) {
option (google.api.http) = {
delete: "/api/v1/{{.TName}}/{id}"
};
}
// update {{.TName}} by id
rpc UpdateByID(Update{{.TableName}}ByIDRequest) returns (Update{{.TableName}}ByIDReply) {
option (google.api.http) = {
put: "/api/v1/{{.TName}}/{id}"
body: "*"
};
}
// get {{.TName}} by id
rpc GetByID(Get{{.TableName}}ByIDRequest) returns (Get{{.TableName}}ByIDReply) {
option (google.api.http) = {
get: "/api/v1/{{.TName}}/{id}"
};
}
// list of {{.TName}} by query parameters
rpc List(List{{.TableName}}Request) returns (List{{.TableName}}Reply) {
option (google.api.http) = {
post: "/api/v1/{{.TName}}/list"
body: "*"
};
}
}
/*
Notes for defining message fields:
1. Suggest using camel case style naming for message field names, such as firstName, lastName, etc.
2. If the message field name ending in 'id', it is recommended to use xxxID naming format, such as userID, orderID, etc.
3. Add validate rules https://github.com/envoyproxy/protoc-gen-validate#constraint-rules, such as:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
If used to generate code that supports the HTTP protocol, notes for defining message fields:
1. If the route contains the path parameter, such as /api/v1/userExample/{id}, the defined
message must contain the name of the path parameter and the name should be added
with a new tag, such as int64 id = 1 [(tagger.tags) = "uri:\"id\""];
2. If the request url is followed by a query parameter, such as /api/v1/getUserExample?name=Tom,
a form tag must be added when defining the query parameter in the message, such as:
string name = 1 [(tagger.tags) = "form:\"name\""].
3. If the message field name contain underscores(such as 'field_name'), it will cause a problem
where the JSON field names of the Swagger request parameters are different from those of the
GRPC JSON tag names. There are two solutions: Solution 1, remove the underline from the
message field name. Option 2, use the tool 'protoc-go-inject-tag' to modify the JSON tag name,
such as: string first_name = 1 ; // @gotags: json:"firstName"
*/
// protoMessageCreateCode
message Create{{.TableName}}Reply {
// createTableReplyFieldCode
}
message Delete{{.TableName}}ByIDRequest {
// deleteTableByIDRequestFieldCode
}
message Delete{{.TableName}}ByIDReply {
}
// protoMessageUpdateCode
message Update{{.TableName}}ByIDReply {
}
// protoMessageDetailCode
message Get{{.TableName}}ByIDRequest {
// getTableByIDRequestFieldCode
}
message Get{{.TableName}}ByIDReply {
{{.TableName}} {{.TName}} = 1;
}
message List{{.TableName}}Request {
api.types.Params params = 1;
}
message List{{.TableName}}Reply {
int64 total = 1;
repeated {{.TableName}} {{.TName}}s = 2;
}
`
protoMessageCreateTmpl *template.Template
protoMessageCreateTmplRaw = `message Create{{.TableName}}Request {
{{- range $i, $v := .Fields}}
{{$v.GoType}} {{$v.JSONName}} = {{$v.AddOne $i}}; {{if $v.Comment}} // {{$v.Comment}}{{end}}
{{- end}}
}`
protoMessageUpdateTmpl *template.Template
protoMessageUpdateTmplRaw = `message Update{{.TableName}}ByIDRequest {
{{- range $i, $v := .Fields}}
{{$v.GoType}} {{$v.JSONName}} = {{$v.AddOneWithTag $i}}; {{if $v.Comment}} // {{$v.Comment}}{{end}}
{{- end}}
}`
protoMessageDetailTmpl *template.Template
protoMessageDetailTmplRaw = `message {{.TableName}} {
{{- range $i, $v := .Fields}}
{{$v.GoType}} {{$v.JSONName}} = {{$v.AddOne $i}}; {{if $v.Comment}} // {{$v.Comment}}{{end}}
{{- end}}
}`
serviceStructTmpl *template.Template
serviceStructTmplRaw = `
{
name: "Create",
fn: func() (interface{}, error) {
// todo enter parameters before testing
// serviceCreateStructCode
},
wantErr: false,
},
{
name: "UpdateByID",
fn: func() (interface{}, error) {
// todo enter parameters before testing
// serviceUpdateStructCode
},
wantErr: false,
},
`
serviceCreateStructTmpl *template.Template
serviceCreateStructTmplRaw = ` req := &serverNameExampleV1.Create{{.TableName}}Request{
{{- range .Fields}}
{{.Name}}: {{.GoTypeZero}}, {{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
return cli.Create(ctx, req)`
serviceUpdateStructTmpl *template.Template
serviceUpdateStructTmplRaw = ` req := &serverNameExampleV1.Update{{.TableName}}ByIDRequest{
{{- range .Fields}}
{{.Name}}: {{.GoTypeZero}}, {{if .Comment}} // {{.Comment}}{{end}}
{{- end}}
}
return cli.UpdateByID(ctx, req)`
tmplParseOnce sync.Once
)
func initTemplate() {
tmplParseOnce.Do(func() {
var err, errSum error
modelStructTmpl, err = template.New("goStruct").Parse(modelStructTmplRaw)
if err != nil {
errSum = errors.Wrap(err, "modelStructTmplRaw")
}
modelTmpl, err = template.New("goFile").Parse(modelTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "modelTmplRaw:"+err.Error())
}
updateFieldTmpl, err = template.New("goUpdateField").Parse(updateFieldTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "updateFieldTmplRaw:"+err.Error())
}
handlerCreateStructTmpl, err = template.New("goPostStruct").Parse(handlerCreateStructTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "handlerCreateStructTmplRaw:"+err.Error())
}
handlerUpdateStructTmpl, err = template.New("goPutStruct").Parse(handlerUpdateStructTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "handlerUpdateStructTmplRaw:"+err.Error())
}
handlerDetailStructTmpl, err = template.New("goGetStruct").Parse(handlerDetailStructTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "handlerDetailStructTmplRaw:"+err.Error())
}
modelJSONTmpl, err = template.New("modelJSON").Parse(modelJSONTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "modelJSONTmplRaw:"+err.Error())
}
protoFileTmpl, err = template.New("protoFile").Parse(protoFileTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileTmplRaw:"+err.Error())
}
protoFileSimpleTmpl, err = template.New("protoFileSimple").Parse(protoFileSimpleTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileSimpleTmplRaw:"+err.Error())
}
protoFileForWebTmpl, err = template.New("protoFileForWeb").Parse(protoFileForWebTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileForWebTmplRaw:"+err.Error())
}
protoFileForSimpleWebTmpl, err = template.New("protoFileForSimpleWeb").Parse(protoFileForSimpleWebTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoFileForSimpleWebTmplRaw:"+err.Error())
}
protoMessageCreateTmpl, err = template.New("protoMessageCreate").Parse(protoMessageCreateTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoMessageCreateTmplRaw:"+err.Error())
}
protoMessageUpdateTmpl, err = template.New("protoMessageUpdate").Parse(protoMessageUpdateTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoMessageUpdateTmplRaw:"+err.Error())
}
protoMessageDetailTmpl, err = template.New("protoMessageDetail").Parse(protoMessageDetailTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "protoMessageDetailTmplRaw:"+err.Error())
}
serviceCreateStructTmpl, err = template.New("serviceCreateStruct").Parse(serviceCreateStructTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "serviceCreateStructTmplRaw:"+err.Error())
}
serviceUpdateStructTmpl, err = template.New("serviceUpdateStruct").Parse(serviceUpdateStructTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "serviceUpdateStructTmplRaw:"+err.Error())
}
serviceStructTmpl, err = template.New("serviceStruct").Parse(serviceStructTmplRaw)
if err != nil {
errSum = errors.Wrap(errSum, "serviceStructTmplRaw:"+err.Error())
}
if errSum != nil {
panic(errSum)
}
})
}
// Package sql2code provides for generating code for different purposes according to sql,
// support generating json, gorm model, update parameter, request parameter code,
// sql can be obtained from parameter, file, db three ways, priority from high to low.
package sql2code
import (
"errors"
"fmt"
"os"
"strings"
"gitlab.wanzhuangkj.com/tush/xpkg/gofile"
"gitlab.wanzhuangkj.com/tush/xpkg/sql2code/parser"
"gitlab.wanzhuangkj.com/tush/xpkg/utils"
)
// Args generate code arguments
type Args struct {
SQL string // DDL sql
DDLFile string // DDL file
DBDriver string // db driver name, such as mysql, mongodb, postgresql, sqlite, default is mysql
DBDsn string // connecting to mysql's dsn, if DBDriver is sqlite, DBDsn is local db file
DBTable string // table name
fieldTypes map[string]string // field name:type
Package string // specify the package name (only valid for model types)
GormType bool // whether to display the gorm type name (only valid for model type codes)
JSONTag bool // does it include a json tag
JSONNamedType int // json field naming type, 0: snake case such as my_field_name, 1: camel sase, such as myFieldName
IsEmbed bool // is gorm.Model embedded
IsWebProto bool // proto file type, true: include router path and swagger info, false: normal proto file without router and swagger
CodeType string // specify the different types of code to be generated, namely model (default), json, dao, handler, proto
ForceTableName bool
Charset string
Collation string
TablePrefix string
ColumnPrefix string
NoNullType bool
NullStyle string
IsExtendedAPI bool // true: generate extended api (9 api), false: generate basic api (5 api)
IsCustomTemplate bool // whether to use custom template, default is false
}
func (a *Args) checkValid() error {
if a.SQL == "" && a.DDLFile == "" && (a.DBDsn == "" && a.DBTable == "") {
return errors.New("you must specify sql or ddl file")
}
if a.DBTable != "" {
tables := strings.Split(a.DBTable, ",")
for _, name := range tables {
if strings.HasSuffix(name, "_test") {
return fmt.Errorf(`the table name (%s) suffix "_test" is not supported for code generation, please delete suffix "_test" or change it to another name. `, name)
}
}
}
if a.DBDriver == "" {
a.DBDriver = parser.DBDriverMysql
} else if a.DBDriver == parser.DBDriverSqlite {
if !gofile.IsExists(a.DBDsn) {
return fmt.Errorf("sqlite db file %s not found in local host", a.DBDsn)
}
}
if a.fieldTypes == nil {
a.fieldTypes = make(map[string]string)
}
return nil
}
func getSQL(args *Args) (string, map[string]string, error) {
if args.SQL != "" {
return args.SQL, nil, nil
}
sql := ""
dbDriverName := strings.ToLower(args.DBDriver)
if args.DDLFile != "" {
if dbDriverName != parser.DBDriverMysql {
return sql, nil, fmt.Errorf("not support driver %s for parsing the sql file, only mysql is supported", args.DBDriver)
}
b, err := os.ReadFile(args.DDLFile)
if err != nil {
return sql, nil, fmt.Errorf("read %s failed, %s", args.DDLFile, err)
}
return string(b), nil, nil
} else if args.DBDsn != "" {
if args.DBTable == "" {
return sql, nil, errors.New("miss database table")
}
switch dbDriverName {
case parser.DBDriverMysql, parser.DBDriverTidb:
dsn := utils.AdaptiveMysqlDsn(args.DBDsn)
sqlStr, err := parser.GetMysqlTableInfo(dsn, args.DBTable)
return sqlStr, nil, err
case parser.DBDriverPostgresql:
dsn := utils.AdaptivePostgresqlDsn(args.DBDsn)
fields, err := parser.GetPostgresqlTableInfo(dsn, args.DBTable)
if err != nil {
return "", nil, err
}
sqlStr, pgTypeMap := parser.ConvertToSQLByPgFields(args.DBTable, fields)
return sqlStr, pgTypeMap, nil
case parser.DBDriverSqlite:
sqlStr, err := parser.GetSqliteTableInfo(args.DBDsn, args.DBTable)
return sqlStr, nil, err
case parser.DBDriverMongodb:
dsn := utils.AdaptiveMongodbDsn(args.DBDsn)
fields, err := parser.GetMongodbTableInfo(dsn, args.DBTable)
if err != nil {
return "", nil, err
}
sqlStr, mongoTypeMap := parser.ConvertToSQLByMgoFields(args.DBTable, fields)
return sqlStr, mongoTypeMap, nil
default:
return "", nil, errors.New("get sql error, unsupported database driver: " + dbDriverName)
}
}
return sql, nil, errors.New("no SQL input(-sql|-f|-db-dsn)")
}
func setOptions(args *Args) []parser.Option {
var opts []parser.Option
if args.DBDriver != "" {
opts = append(opts, parser.WithDBDriver(args.DBDriver))
}
if args.fieldTypes != nil {
opts = append(opts, parser.WithFieldTypes(args.fieldTypes))
}
if args.Charset != "" {
opts = append(opts, parser.WithCharset(args.Charset))
}
if args.Collation != "" {
opts = append(opts, parser.WithCollation(args.Collation))
}
if args.JSONTag {
opts = append(opts, parser.WithJSONTag(args.JSONNamedType))
}
if args.TablePrefix != "" {
opts = append(opts, parser.WithTablePrefix(args.TablePrefix))
}
if args.ColumnPrefix != "" {
opts = append(opts, parser.WithColumnPrefix(args.ColumnPrefix))
}
if args.NoNullType {
opts = append(opts, parser.WithNoNullType())
}
if args.IsEmbed {
opts = append(opts, parser.WithEmbed())
}
if args.IsWebProto {
opts = append(opts, parser.WithWebProto())
}
if args.NullStyle != "" {
switch args.NullStyle {
case "sql":
opts = append(opts, parser.WithNullStyle(parser.NullInSql))
case "ptr":
opts = append(opts, parser.WithNullStyle(parser.NullInPointer))
default:
fmt.Printf("invalid null style: %s\n", args.NullStyle)
return nil
}
} else {
opts = append(opts, parser.WithNullStyle(parser.NullDisable))
}
if args.Package != "" {
opts = append(opts, parser.WithPackage(args.Package))
}
if args.GormType {
opts = append(opts, parser.WithGormType())
}
if args.ForceTableName {
opts = append(opts, parser.WithForceTableName())
}
if args.IsExtendedAPI {
opts = append(opts, parser.WithExtendedAPI())
}
if args.IsCustomTemplate {
opts = append(opts, parser.WithCustomTemplate())
}
return opts
}
// GenerateOne generate gorm code from sql, which can be obtained from parameters, files and db, with priority from highest to lowest
func GenerateOne(args *Args) (string, error) {
codes, err := Generate(args)
if err != nil {
return "", err
}
if args.CodeType == "" {
args.CodeType = parser.CodeTypeModel // default is model code
}
out, ok := codes[args.CodeType]
if !ok {
return "", fmt.Errorf("unknown code type %s", args.CodeType)
}
return out, nil
}
// Generate model, json, dao, handler, proto codes
func Generate(args *Args) (map[string]string, error) {
if err := args.checkValid(); err != nil {
return nil, err
}
sql, fieldTypes, err := getSQL(args)
if err != nil {
return nil, err
}
if fieldTypes != nil {
args.fieldTypes = fieldTypes
}
if sql == "" {
return nil, fmt.Errorf("get sql from %s error, maybe the table %s doesn't exist", args.DBDriver, args.DBTable)
}
opt := setOptions(args)
return parser.ParseSQL(sql, opt...)
}
package sql2code
import (
"testing"
"github.com/stretchr/testify/assert"
)
var sqlData = `
create table user
(
id bigint unsigned auto_increment
primary key,
created_at datetime null,
updated_at datetime null,
deleted_at datetime null,
name char(50) not null comment 'username',
password char(100) not null comment 'password',
email char(50) not null comment 'email',
phone bigint unsigned not null comment 'phone number',
age tinyint not null comment 'age',
gender tinyint not null comment 'gender, 1:male, 2:female, 3:unknown',
constraint user_email_uindex
unique (email)
);
`
func TestGenerateOne(t *testing.T) {
type args struct {
args *Args
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "sql form param",
args: args{args: &Args{
SQL: sqlData,
}},
wantErr: false,
},
{
name: "sql from file",
args: args{args: &Args{
DDLFile: "test.sql",
}},
wantErr: false,
},
//{
// name: "sql from db",
// args: args{args: &Args{
// DBDsn: "root:123456@(192.168.3.37:3306)/test",
// DBTable: "user",
// }},
// wantErr: false,
//},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GenerateOne(tt.args.args)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateOne() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Log(got)
})
}
}
func TestGenerate(t *testing.T) {
type args struct {
args *Args
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "sql form param",
args: args{args: &Args{
SQL: sqlData,
}},
wantErr: false,
},
//{
// name: "sql from sqlite",
// args: args{args: &Args{
// DBDsn: "C:\\Users\\zhuyasen\\Desktop\\genTest\\sql\\sqlite\\xmall.db",
// DBTable: "user",
// DBDriver: "sqlite",
// }},
// wantErr: false,
//},
//{
// name: "sql from mysql",
// args: args{args: &Args{
// DBDsn: "root:123456@(192.168.3.37:3306)/account",
// DBTable: "user",
// DBDriver: "mysql",
// }},
// wantErr: false,
//},
//{
// name: "sql from postgresql",
// args: args{args: &Args{
// DBDsn: "root:123456@(192.168.3.37:5432)/account",
// DBTable: "user",
// DBDriver: "postgresql",
// }},
// wantErr: false,
//},
//{
// name: "sql from mongodb",
// args: args{args: &Args{
// DBDsn: "root:123456@(192.168.3.37:27017)/account",
// DBTable: "people",
// DBDriver: "mongodb",
// IsCustomTemplate: true,
// }},
// wantErr: false,
//},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Generate(tt.args.args)
if (err != nil) != tt.wantErr {
t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Log(got)
})
}
}
func TestGenerateError(t *testing.T) {
a := &Args{}
_, err := Generate(a)
assert.Error(t, err)
_, err = GenerateOne(a)
assert.Error(t, err)
a = &Args{DDLFile: "notfound.sql"}
_, err = Generate(a)
assert.Error(t, err)
a = &Args{DBDsn: "root:123456@(127.0.0.1:3306)/test"}
_, err = Generate(a)
assert.Error(t, err)
a = &Args{DBDsn: "root:123456@(127.0.0.1:3306)/test", DBTable: "user"}
_, err = Generate(a)
assert.Error(t, err)
a = &Args{DDLFile: "test.sql", CodeType: "unknown"}
_, err = GenerateOne(a)
t.Log(err)
assert.Error(t, err)
}
func Test_getOptions(t *testing.T) {
a := &Args{
Package: "Package",
GormType: true,
JSONTag: true,
ForceTableName: true,
Charset: "Charset",
Collation: "Collation",
TablePrefix: "TablePrefix",
ColumnPrefix: "ColumnPrefix",
NoNullType: true,
NullStyle: "sql",
IsWebProto: true,
IsExtendedAPI: true,
}
o := setOptions(a)
assert.NotNil(t, o)
a.NullStyle = "ptr"
assert.NotNil(t, o)
a.NullStyle = "default"
assert.NotNil(t, o)
}
create table user
(
id bigint unsigned auto_increment
primary key,
created_at datetime null,
updated_at datetime null,
deleted_at datetime null,
name char(50) not null comment 'username',
password char(100) not null comment 'password',
email char(50) not null comment 'email',
phone bigint unsigned not null comment 'phone number',
age tinyint not null comment 'age',
gender tinyint not null comment 'gender, 1:male, 2:female, 3:unknown',
constraint user_email_uindex
unique (email)
);
package xgorm
import (
"reflect"
"time"
"github.com/huandu/xstrings"
"gorm.io/gorm"
)
// Model embedded structs, add `gorm: "embedded"` when defining table structs
type Model struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// Model2 embedded structs, json tag named is snake case
type Model2 struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// KV map type
type KV = map[string]interface{}
// GetTableName get table name
func GetTableName(object interface{}) string {
tableName := ""
typeof := reflect.TypeOf(object)
switch typeof.Kind() {
case reflect.Ptr:
tableName = typeof.Elem().Name()
case reflect.Struct:
tableName = typeof.Name()
default:
return tableName
}
return xstrings.ToSnakeCase(tableName)
}
// Package ggorm is a library wrapped on top of gorm.io/gorm, with added features such as link tracing, paging queries, etc.
package xgorm
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"time"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
mysqlDriver "gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gorm.io/plugin/dbresolver"
)
type DB = gorm.DB
const (
// DBDriverMysql mysql driver
DBDriverMysql = "mysql"
// DBDriverPostgresql postgresql driver
DBDriverPostgresql = "postgresql"
// DBDriverTidb tidb driver
DBDriverTidb = "tidb"
// DBDriverSqlite sqlite driver
DBDriverSqlite = "sqlite"
)
// InitMysql init mysql or tidb
func InitMysql(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
sqlDB, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(o.maxIdleConns) // set the maximum number of connections in the idle connection pool
sqlDB.SetMaxOpenConns(o.maxOpenConns) // set the maximum number of open database connections
sqlDB.SetConnMaxLifetime(o.connMaxLifetime) // set the maximum time a connection can be reused
db, err := gorm.Open(mysqlDriver.New(mysqlDriver.Config{Conn: sqlDB}), gormConfig(o))
if err != nil {
return nil, err
}
db.Set("gorm:table_options", "CHARSET=utf8mb4") // automatic appending of table suffixes when creating tables
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register read-write separation plugin
if len(o.slavesDsn) > 0 {
err = db.Use(rwSeparationPlugin(o))
if err != nil {
return nil, err
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// InitPostgresql init postgresql
func InitPostgresql(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
db, err := gorm.Open(postgres.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register read-write separation plugin
if len(o.slavesDsn) > 0 {
err = db.Use(rwSeparationPlugin(o))
if err != nil {
return nil, err
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// InitTidb init tidb
func InitTidb(dsn string, opts ...Option) (*gorm.DB, error) {
return InitMysql(dsn, opts...)
}
// InitSqlite init sqlite
func InitSqlite(dbFile string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
dsn := fmt.Sprintf("%s?_journal=WAL&_vacuum=incremental", dbFile)
db, err := gorm.Open(sqlite.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
db.Set("gorm:auto_increment", true)
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// CloseDB close gorm db
func CloseDB(db *gorm.DB) error {
if db == nil {
return nil
}
sqlDB, err := db.DB()
if err != nil {
return err
}
checkInUse(sqlDB, time.Second*5)
return sqlDB.Close()
}
func checkInUse(sqlDB *sql.DB, duration time.Duration) {
ctx, _ := context.WithTimeout(context.Background(), duration) //nolint
for {
select {
case <-time.After(time.Millisecond * 250):
if v := sqlDB.Stats().InUse; v == 0 {
return
}
case <-ctx.Done():
return
}
}
}
// CloseSQLDB close sql db
func CloseSQLDB(db *gorm.DB) {
sqlDB, err := db.DB()
if err != nil {
return
}
_ = sqlDB.Close()
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = NewCustomGormLogger(o)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
func rwSeparationPlugin(o *options) gorm.Plugin {
slaves := []gorm.Dialector{}
for _, dsn := range o.slavesDsn {
slaves = append(slaves, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
masters := []gorm.Dialector{}
for _, dsn := range o.mastersDsn {
masters = append(masters, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
return dbresolver.Register(dbresolver.Config{
Sources: masters,
Replicas: slaves,
Policy: dbresolver.RandomPolicy{},
})
}
package xgorm
import (
"context"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm/logger"
"gorm.io/gorm/utils"
)
type gormLogger struct {
gLog *zap.Logger
requestIDKey string
logLevel logger.LogLevel
}
// NewCustomGormLogger custom gorm logger
func NewCustomGormLogger(o *options) logger.Interface {
return &gormLogger{
gLog: o.gLog,
requestIDKey: o.requestIDKey,
logLevel: o.logLevel,
}
}
// LogMode log mode
func (l *gormLogger) LogMode(level logger.LogLevel) logger.Interface {
l.logLevel = level
return l
}
// Info print info
func (l *gormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Info {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Info(msg, zap.Any("data", data), zap.String("line", utils.FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Warn print warn messages
func (l *gormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Warn {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", utils.FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Error print error messages
func (l *gormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Error {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", utils.FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
const (
FileLine = "file"
)
// Trace print sql message
func (l *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.logLevel <= logger.Silent {
return
}
cost := time.Since(begin).String()
sql, rows := fc()
var rowsField zap.Field
if rows == -1 {
rowsField = zap.String("rows", "-")
} else {
rowsField = zap.Int64("rows", rows)
}
var fileLineField zap.Field
fileLine := utils.FileWithLineNum()
ss := strings.Split(fileLine, "/internal/")
if len(ss) == 2 {
fileLineField = zap.String(FileLine, ss[1])
} else {
fileLineField = zap.String(FileLine, fileLine)
}
if err != nil {
l.gLog.Warn("gorm",
zap.Error(err),
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Info {
l.gLog.Info("gorm",
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Warn {
l.gLog.Warn("gorm",
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
}
}
func requestIDField(ctx context.Context, requestIDKey string) zap.Field {
if requestIDKey == "" {
return zap.Skip()
}
var field zap.Field
if requestIDKey != "" {
if v, ok := ctx.Value(requestIDKey).(string); ok {
field = zap.String(requestIDKey, v)
} else {
field = zap.Skip()
}
}
return field
}
package xgorm
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"gorm.io/gorm/logger"
)
func TestNewCustomGormLogger(t *testing.T) {
zapLog, _ := zap.NewDevelopment()
l := NewCustomGormLogger(&options{
requestIDKey: "request_id",
gLog: zapLog,
logLevel: logger.Info,
})
l.LogMode(logger.Info)
ctx := context.WithValue(context.Background(), "request_id", "123")
l.Info(ctx, "info", "foo")
l.Warn(ctx, "warn", "bar")
l.Error(ctx, "error", "foo bar")
l.LogMode(logger.Silent)
l.Trace(ctx, time.Now(), nil, nil)
l.LogMode(logger.Info)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", -1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, errors.New("Error 1054: Unknown column 'test_column'"))
l.LogMode(logger.Warn)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
}
func Test_requestIDField(t *testing.T) {
ctx := context.WithValue(context.Background(), "request_id", "123")
field := requestIDField(ctx, "")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "your request id key")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "request_id")
assert.Equal(t, zap.String("request_id", "123"), field)
}
package xgorm
import (
"database/sql"
"fmt"
"gorm.io/gorm"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var dsn = "root:123456@(192.168.3.37:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
func TestInitMysql(t *testing.T) {
db, err := InitMysql(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real mysql
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func TestInitTidb(t *testing.T) {
db, err := InitTidb(dsn)
if err != nil {
// ignore test error about not being able to connect to real tidb
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func TestInitSqlite(t *testing.T) {
dbFile := "test_sqlite.db"
db, err := InitSqlite(dbFile)
if err != nil {
// ignore test error about not being able to connect to real sqlite
t.Logf(fmt.Sprintf("connect to sqlite failed, err=%v, dbFile=%s", err, dbFile))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func TestInitPostgresql(t *testing.T) {
dsn = "host=192.168.3.37 user=root password=123456 dbname=account port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := InitPostgresql(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real postgresql
t.Logf(fmt.Sprintf("connect to postgresql failed, err=%v, dsn=%s", err, dsn))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithRWSeparation([]string{
"root:123456@(192.168.3.37:3306)/slave1",
"root:123456@(192.168.3.37:3306)/slave2"},
"root:123456@(192.168.3.37:3306)/master"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
err := rwSeparationPlugin(o)
assert.NotNil(t, err)
}
type userExample struct {
Model `gorm:"embedded"`
Name string `gorm:"type:varchar(40);unique_index;not null" json:"name"`
Age int `gorm:"not null" json:"age"`
Gender string `gorm:"type:varchar(10);not null" json:"gender"`
}
func TestGetTableName(t *testing.T) {
name := GetTableName(&userExample{})
assert.NotEmpty(t, name)
name = GetTableName(userExample{})
assert.NotEmpty(t, name)
name = GetTableName("table")
assert.Empty(t, name)
}
func TestCloseDB(t *testing.T) {
sqlDB := new(sql.DB)
checkInUse(sqlDB, time.Millisecond*100)
checkInUse(sqlDB, time.Millisecond*600)
db := new(gorm.DB)
defer func() { recover() }()
_ = CloseDB(db)
}
func TestCloseSqlDB(t *testing.T) {
db := new(gorm.DB)
defer func() { recover() }()
CloseSQLDB(db)
}
package xgorm
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
slavesDsn []string
mastersDsn []string
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithRWSeparation setting read-write separation
func WithRWSeparation(slavesDsn []string, mastersDsn ...string) Option {
return func(o *options) {
o.slavesDsn = slavesDsn
o.mastersDsn = mastersDsn
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}
package query
import "strings"
var defaultMaxSize = 1000
// SetMaxSize change the default maximum number of pages per page
func SetMaxSize(max int) {
if max < 10 {
max = 10
}
defaultMaxSize = max
}
// Page info
type Page struct {
page int // page number, starting from page 0
limit int // number per page
sort string // sort fields, default is id backwards, you can add - sign before the field to indicate reverse order, no - sign to indicate ascending order, multiple fields separated by comma
}
// Page get page value
func (p *Page) Page() int {
return p.page
}
// Limit number per page
func (p *Page) Limit() int {
return p.limit
}
// Size number per page
// Deprecated: use Limit instead
func (p *Page) Size() int {
return p.limit
}
// Sort get sort field
func (p *Page) Sort() string {
return p.sort
}
// Offset get offset value
func (p *Page) Offset() int {
return p.page * p.limit
}
// DefaultPage default page, number 20 per page, sorted by id backwards
func DefaultPage(page int) *Page {
if page < 0 {
page = 0
}
return &Page{
page: page,
limit: 20,
sort: "id DESC",
}
}
// NewPage custom page, starting from page 0.
// the parameter columnNames indicates a sort field, if empty means id descending,
// if there are multiple column names, separated by a comma,
// a '-' sign in front of each column name indicates descending order, otherwise ascending order.
func NewPage(page int, limit int, columnNames string) *Page {
if page < 0 {
page = 0
}
if limit > defaultMaxSize || limit < 1 {
limit = defaultMaxSize
}
return &Page{
page: page,
limit: limit,
sort: getSort(columnNames),
}
}
// convert to mysql sort, each column name preceded by a '-' sign, indicating descending order, otherwise ascending order, example:
//
// columnNames="name" means sort by name in ascending order,
// columnNames="-name" means sort by name descending,
// columnNames="name,age" means sort by name in ascending order, otherwise sort by age in ascending order,
// columnNames="-name,-age" means sort by name descending before sorting by age descending.
func getSort(columnNames string) string {
columnNames = strings.Replace(columnNames, " ", "", -1)
if columnNames == "" {
return "id DESC"
}
names := strings.Split(columnNames, ",")
strs := make([]string, 0, len(names))
for _, name := range names {
if name[0] == '-' && len(name) > 1 {
strs = append(strs, name[1:]+" DESC")
} else {
strs = append(strs, name+" ASC")
}
}
return strings.Join(strs, ", ")
}
// Package query is a library of custom condition queries, support for complex conditional paging queries.
package query
import (
"fmt"
"strings"
)
const (
// Eq equal
Eq = "eq"
// Neq not equal
Neq = "neq"
// Gt greater than
Gt = "gt"
// Gte greater than or equal
Gte = "gte"
// Lt less than
Lt = "lt"
// Lte less than or equal
Lte = "lte"
// Like fuzzy lookup
Like = "like"
// In include
In = "in"
// AND logic and
AND string = "and"
// OR logic or
OR string = "or"
)
var expMap = map[string]string{
Eq: " = ",
Neq: " <> ",
Gt: " > ",
Gte: " >= ",
Lt: " < ",
Lte: " <= ",
Like: " LIKE ",
In: " IN ",
"=": " = ",
"!=": " <> ",
">": " > ",
">=": " >= ",
"<": " < ",
"<=": " <= ",
}
var logicMap = map[string]string{
AND: " AND ",
OR: " OR ",
"&": " AND ",
"&&": " AND ",
"|": " OR ",
"||": " OR ",
"AND": " AND ",
"OR": " OR ",
}
// Params query parameters
type Params struct {
Page int `json:"page" form:"page" binding:"gte=0"`
Limit int `json:"limit" form:"limit" binding:"gte=1"`
Sort string `json:"sort,omitempty" form:"sort" binding:""`
Columns []Column `json:"columns,omitempty" form:"columns"` // not required
// Deprecated: use Limit instead in xmall version v1.8.6, will remove in the future
Size int `json:"size" form:"size"`
}
// Column query info
type Column struct {
Name string `json:"name" form:"name"` // column name
Exp string `json:"exp" form:"exp"` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
Value interface{} `json:"value" form:"value"` // column value
Logic string `json:"logic" form:"logic"` // logical type, defaults to and when the value is null, with &(and), ||(or)
}
func (c *Column) checkValid() error {
if c.Name == "" {
return fmt.Errorf("field 'name' cannot be empty")
}
if c.Value == nil {
return fmt.Errorf("field 'value' cannot be nil")
}
return nil
}
// converting ExpType to sql expressions and LogicType to sql using characters
func (c *Column) convert() error {
if c.Exp == "" {
c.Exp = Eq
}
if v, ok := expMap[strings.ToLower(c.Exp)]; ok { //nolint
c.Exp = v
if c.Exp == " LIKE " {
c.Value = fmt.Sprintf("%%%v%%", c.Value)
}
if c.Exp == " IN " {
val, ok := c.Value.(string)
if !ok {
return fmt.Errorf("invalid value type '%s'", c.Value)
}
iVal := []interface{}{}
ss := strings.Split(val, ",")
for _, s := range ss {
iVal = append(iVal, s)
}
c.Value = iVal
}
} else {
return fmt.Errorf("unknown exp type '%s'", c.Exp)
}
if c.Logic == "" {
c.Logic = AND
}
if v, ok := logicMap[strings.ToLower(c.Logic)]; ok { //nolint
c.Logic = v
} else {
return fmt.Errorf("unknown logic type '%s'", c.Logic)
}
return nil
}
// ConvertToPage converted to page
func (p *Params) ConvertToPage() (order string, limit int, offset int) { //nolint
page := NewPage(p.Page, p.Limit, p.Sort)
order = page.sort
limit = page.limit
offset = page.page * page.limit
return //nolint
}
// ConvertToGormConditions conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (p *Params) ConvertToGormConditions() (string, []interface{}, error) {
str := ""
args := []interface{}{}
l := len(p.Columns)
if l == 0 {
return "", nil, nil
}
isUseIN := true
if l == 1 {
isUseIN = false
}
field := p.Columns[0].Name
for i, column := range p.Columns {
if err := column.checkValid(); err != nil {
return "", nil, err
}
err := column.convert()
if err != nil {
return "", nil, err
}
symbol := "?"
if column.Exp == " IN " {
symbol = "(?)"
}
if i == l-1 { // ignore the logical type of the last column
str += column.Name + column.Exp + symbol
} else {
str += column.Name + column.Exp + symbol + column.Logic
}
args = append(args, column.Value)
// when multiple columns are the same, determine whether the use of IN
if isUseIN {
if field != column.Name {
isUseIN = false
continue
}
if column.Exp != expMap[Eq] {
isUseIN = false
}
}
}
if isUseIN {
str = field + " IN (?)"
args = []interface{}{args}
}
return str, args, nil
}
// Conditions query conditions
type Conditions struct {
Columns []Column `json:"columns" form:"columns" binding:"min=1"` // columns info
}
// CheckValid check valid
func (c *Conditions) CheckValid() error {
if len(c.Columns) == 0 {
return fmt.Errorf("field 'columns' cannot be empty")
}
for _, column := range c.Columns {
err := column.checkValid()
if err != nil {
return err
}
if column.Exp != "" {
if _, ok := expMap[column.Exp]; !ok {
return fmt.Errorf("unknown exp type '%s'", column.Exp)
}
}
if column.Logic != "" {
if _, ok := logicMap[column.Logic]; !ok {
return fmt.Errorf("unknown logic type '%s'", column.Logic)
}
}
}
return nil
}
// ConvertToGorm conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (c *Conditions) ConvertToGorm() (string, []interface{}, error) {
p := &Params{Columns: c.Columns}
return p.ConvertToGormConditions()
}
package query
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPage(t *testing.T) {
page := DefaultPage(-1)
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
SetMaxSize(1)
page = NewPage(-1, 100, "id")
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
}
func TestParams_ConvertToPage(t *testing.T) {
p := &Params{
Page: 1,
Limit: 50,
Sort: "age,-name",
}
order, limit, offset := p.ConvertToPage()
t.Logf("order=%s, limit=%d, offset=%d", order, limit, offset)
}
func TestParams_ConvertToGormConditions(t *testing.T) {
type args struct {
columns []Column
}
tests := []struct {
name string
args args
want string
want1 []interface{}
wantErr bool
}{
// --------------------------- only 1 column query ------------------------------
{
name: "1 column eq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
},
},
want: "name = ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column neq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: "neq",
Exp: "!=",
},
},
},
want: "name <> ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column gt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "age > ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column gte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "age >= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "age < ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age <= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column like",
args: args{
columns: []Column{
{
Name: "name",
Value: "Li",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"%Li%"},
wantErr: false,
},
{
name: "1 column IN",
args: args{
columns: []Column{
{
Name: "name",
Value: "ab,cd,ef",
Exp: In,
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"ab", "cd", "ef"}},
wantErr: false,
},
// --------------------------- query 2 columns ------------------------------
{
name: "2 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
},
},
want: "name = ? AND gender = ?",
want1: []interface{}{"ZhangSan", "male"},
wantErr: false,
},
{
name: "2 columns neq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: Neq,
Exp: "!=",
},
{
Name: "name",
Value: "LiSi",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name <> ? AND name <> ?",
want1: []interface{}{"ZhangSan", "LiSi"},
wantErr: false,
},
{
name: "2 columns gt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "gender = ? AND age > ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns gte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "gender = ? AND age >= ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns lt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "gender = ? AND age < ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns lte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "gender = ? AND age <= ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns range and",
args: args{
columns: []Column{
{
Name: "age",
Value: 10,
//Exp: Gte,
Exp: ">=",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age >= ? AND age <= ?",
want1: []interface{}{10, 20},
wantErr: false,
},
{
name: "2 columns eq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "female",
},
},
},
want: "name = ? OR gender = ?",
want1: []interface{}{"LiSi", "female"},
wantErr: false,
},
{
name: "2 columns neq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "male",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name = ? OR gender <> ?",
want1: []interface{}{"LiSi", "male"},
wantErr: false,
},
{
name: "2 columns eq and in",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
//Logic: "&",
},
{
Name: "name",
Value: "LiSi,ZhangSan,WangWu",
Exp: In,
},
},
},
want: "gender = ? AND name IN (?)",
want1: []interface{}{"male", []interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ------------------------------ IN -------------------------------------------------
{
name: "3 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
},
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "name",
Value: "WangWu",
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ---------------------------- error ----------------------------------------------
{
name: "exp type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Exp: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "logic type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "empty",
args: args{
columns: nil,
},
want: "",
want1: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := &Params{
Columns: tt.args.columns,
}
got, got1, err := params.ConvertToGormConditions()
if (err != nil) != tt.wantErr {
t.Errorf("ConvertToGormConditions() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ConvertToGormConditions() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("ConvertToGormConditions() got1 = %v, want %v", got1, tt.want1)
}
got = strings.Replace(got, "?", "%v", -1)
t.Logf(got, got1...)
})
}
}
func TestConditions_ConvertToGorm(t *testing.T) {
c := Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
str, values, err := c.ConvertToGorm()
if err != nil {
t.Error(err)
}
assert.Equal(t, "name = ? AND gender = ?", str)
assert.Equal(t, len(values), 2)
}
func TestConditions_checkValid(t *testing.T) {
// empty error
c := Conditions{}
err := c.CheckValid()
assert.Error(t, err)
// value is empty error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: nil,
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// exp error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Exp: "unknown-exp",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// logic error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Logic: "unknown-logic",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// success
c = Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
err = c.CheckValid()
assert.NoError(t, err)
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论