feat: implement sponge commands

This commit is contained in:
zhuyasen 2022-10-17 23:11:21 +08:00
parent 552d7b89e8
commit f67e29c6e1
108 changed files with 6693 additions and 1356 deletions

View File

@ -1,27 +0,0 @@
name: Test and coverage
on:
# 提交到main分支时触发执行jobs
push:
branches:
- main
# 合并到main分支时触发执行jobs
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: 1.19
id: go
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Run coverage
run: go test -race -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v /vendor/ | grep -v /api/)
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3

View File

@ -1,29 +0,0 @@
name: Go
on:
# 提交到main分支时触发执行jobs
push:
branches:
- main
# 合并到main分支时触发执行jobs
pull_request:
branches:
- main
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: 1.19
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Get dependencies
run: go mod tidy
env:
GOPROXY: https://proxy.golang.org
- name: Build
run: go build -v cmd/serverNameExample/main.go

71
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,71 @@
name: Test and Build
on:
# triggers the execution of jobs when committed to the main branch
push:
branches:
- main
# trigger execution of jobs when merging to main branch
pull_request:
branches:
- main
jobs:
lint:
name: Golangci-lint
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Lint Go Code
run: |
export PATH=$PATH:$(go env GOPATH)/bin
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.49.0
make ci-lint
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Run Unit tests
run: make test
- name: Run coverage
run: go test -race -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v /vendor/ | grep -v /api/)
- name: Upload Coverage report to CodeCov
uses: codecov/codecov-action@v3
with:
file: ./coverage.txt
#token: ${{secrets.CODECOV_TOKEN}}
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Build
run: make build && make build-sponge

View File

@ -1,34 +0,0 @@
# 主要用于release
# see: https://www.qikqiak.com/post/use-github-actions-build-go-app/
# https://goreleaser.com/
name: Release
on:
push:
tags:
- v*
jobs:
releases-matrix:
name: Release goctl binary
runs-on: ubuntu-latest
strategy:
matrix:
# build and publish in parallel: linux/386, linux/amd64, linux/arm64,
# windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
goos: [ linux, windows, darwin ]
goarch: [ "386", amd64, arm64 ]
exclude:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v3
- uses: zhufuyi/sponge@sponge
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.19.linux-amd64.tar.gz"
project_path: "./"
binary_name: "sponge"
extra_files: README.md

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
*.dylib
*.log
cmd/serverNameExample/serverNameExample
cmd/sponge/sponge
*@@*@@*
coverage.txt

View File

@ -61,11 +61,45 @@ cover:
go tool cover -html=cover.out
.PHONY: docs
# generate swagger docs, the host address can be changed via parameters, e.g. make docs HOST=192.168.3.37
docs: mod fmt
@bash scripts/swag-docs.sh $(HOST)
.PHONY: graph
# generate interactive visual function dependency graphs
graph:
@echo "generating graph ......"
@cp -f cmd/serverNameExample/main.go .
go-callvis -skipbrowser -format=svg -nostd -file=serverNameExample github.com/zhufuyi/sponge
@rm -f main.go serverNameExample.gv
.PHONY: proto
# generate *.pb.go codes from *.proto files
proto: mod fmt
@bash scripts/protoc.sh
.PHONY: proto-doc
# generate doc from *.proto files
proto-doc:
@bash scripts/proto-doc.sh
.PHONY: build
# go build the linux amd64 binary file
# build serverNameExample for linux amd64 binary
build:
@echo "building 'serverNameExample', binary file will output to 'cmd/serverNameExample'"
@cd cmd/serverNameExample && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -gcflags "all=-N -l"
@echo "build finished, binary file in path 'cmd/serverNameExample'"
.PHONY: build-sponge
# build sponge for linux amd64 binary
build-sponge:
@echo "building 'sponge', binary file will output to 'cmd/sponge'"
@cd cmd/sponge && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build
.PHONY: run
@ -118,38 +152,12 @@ deploy-docker:
# clean binary file, cover.out, redundant dependency packages
clean:
@rm -vrf cmd/serverNameExample/serverNameExample
@rm -vrf cmd/sponge/sponge
@rm -vrf cover.out
go mod tidy
@echo "clean finished"
.PHONY: docs
# generate swagger docs, the host address can be changed via parameters, e.g. make docs HOST=192.168.3.37
docs: mod fmt
@bash scripts/swag-docs.sh $(HOST)
.PHONY: graph
# generate interactive visual function dependency graphs
graph:
@echo "generating graph ......"
@cp -f cmd/serverNameExample/main.go .
go-callvis -skipbrowser -format=svg -nostd -file=serverNameExample github.com/zhufuyi/sponge
@rm -f main.go serverNameExample.gv
.PHONY: proto
# generate *.pb.go codes from *.proto files
proto: mod fmt
@bash scripts/protoc.sh
.PHONY: proto-doc
# generate doc from *.proto files
proto-doc:
@bash scripts/proto-doc.sh
# show help
help:
@echo ''

View File

@ -14,7 +14,7 @@
</div>
**sponge** is a go microservices framework, a tool for quickly creating complete microservices codes for http or grpc. Generate `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` codes from the SQL DDL, which can be combined into full services(similar to how a broken sponge cell automatically reorganises itself into a new sponge).
**sponge** is a microservices framework for quickly creating http or grpc code. Generate codes `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` from SQL DDL, these codes can be combined into complete services (similar to how a broken sponge cell can automatically reorganize into a new sponge).
Features :
@ -22,17 +22,17 @@ Features :
- RPC framework [grpc](https://github.com/grpc/grpc-go)
- Configuration file parsing [viper](https://github.com/spf13/viper)
- Configuration Center [nacos](https://github.com/alibaba/nacos)
- Logging [zap](go.uber.org/zap)
- Database component [gorm](gorm.io/gorm)
- Caching component [go-redis](github.com/go-redis/redis)
- Documentation [swagger](github.com/swaggo/swag)
- Authorization [authorization](github.com/golang-jwt/jwt)
- Validator [validator](github.com/go-playground/validator)
- Rate limiter [ratelimiter](golang.org/x/time/rate)
- Circuit Breaker [hystrix](github.com/afex/hystrix-go)
- Tracking [opentelemetry](go.opentelemetry.io/otel)
- Monitoring [prometheus](github.com/prometheus/client_golang/prometheus) [grafana](https://github.com/grafana/grafana)
- Service registration and discovery [etcd](https://github.com/etcd-io/etcd), [consul](https://github.com/hashicorp/consul), [nacos](https://github.com/alibaba/) nacos)
- Logging [zap](https://go.uber.org/zap)
- Database component [gorm](https://gorm.io/gorm)
- Caching component [go-redis](https://github.com/go-redis/redis)
- Documentation [swagger](https://github.com/swaggo/swag)
- Authorization [authorization](https://github.com/golang-jwt/jwt)
- Validator [validator](https://github.com/go-playground/validator)
- Rate limiter [aegis](https://github.com/go-kratos/aegis), [rate](https://golang.org/x/time/rate)
- Circuit Breaker [aegis](https://github.com/go-kratos/aegis)
- Tracking [opentelemetry](https://go.opentelemetry.io/otel)
- Monitoring [prometheus](https://github.com/prometheus/client_golang/prometheus), [grafana](https://github.com/grafana/grafana)
- Service registration and discovery [etcd](https://github.com/etcd-io/etcd), [consul](https://github.com/hashicorp/consul), [nacos](https://github.com/alibaba/)
- Performance analysis [go profile](https://go.dev/blog/pprof)
- Code inspection [golangci-lint](https://github.com/golangci/golangci-lint)
- Continuous Integration CI [jenkins](https://github.com/jenkinsci/jenkins)
@ -78,7 +78,15 @@ The development specification follows the [Uber Go Language Coding Specification
### Install
> go install github.com/zhufuyi/sponge@sponge
```bash
go install github.com/zhufuyi/sponge/cmd/sponge@latest
sponge update
```
<br>
### Quickly create a http project
@ -106,7 +114,7 @@ Way 1: Run locally in the binary
Copy `http://localhost:8080/swagger/index.html` to your browser and test the api interface.
Way 2: Run in docker
Way 2: Run in docker. Prerequisite: `docker` and `docker-compose` are already installed.
```bash
# Build the docker image
@ -120,7 +128,7 @@ cd deployments/docker-compose
docker-compose ps
```
Way 3: Run in k8s
Way 3: Run in k8s. Prerequisite: `docker` and `kubectl` are already installed.
```bash
# Build the image
@ -135,7 +143,7 @@ make deploy-k8s
# Check the status of the service
kubectl get -f account-deployment.yml
```
```
You can also use Jenkins to automatically build deployments to k8s.
@ -208,9 +216,9 @@ make deploy-k8s
# Check the status of the service
kubectl get -f account-deployment.yml
```
```
You can also use Jenkins to automatically build deployments to k8s.
You can also use Jenkins to automatically build deployments to k8s.
<br>

View File

@ -59,13 +59,7 @@ func registerInits() []app.Init {
_, _ = logger.Init(
logger.WithLevel(config.Get().Logger.Level),
logger.WithFormat(config.Get().Logger.Format),
logger.WithSave(config.Get().Logger.IsSave,
logger.WithFileName(config.Get().Logger.LogFileConfig.Filename),
logger.WithFileMaxSize(config.Get().Logger.LogFileConfig.MaxSize),
logger.WithFileMaxBackups(config.Get().Logger.LogFileConfig.MaxBackups),
logger.WithFileMaxAge(config.Get().Logger.LogFileConfig.MaxAge),
logger.WithFileIsCompression(config.Get().Logger.LogFileConfig.IsCompression),
),
logger.WithSave(config.Get().Logger.IsSave),
)
var inits []app.Init
@ -171,6 +165,7 @@ func grpcOptions() []server.GRPCOption {
return opts
}
// 使用etcd实例化服务注册consul和nacos也类似
func getETCDRegistry(etcdEndpoints []string, instanceName string, instanceEndpoints []string) (registry.Registry, *registry.ServiceInstance) {
serviceInstance := registry.NewServiceInstance(instanceName, instanceEndpoints)

View File

@ -0,0 +1,148 @@
package generate
import (
"bytes"
"fmt"
"strings"
"github.com/zhufuyi/sponge/pkg/gofile"
"github.com/zhufuyi/sponge/pkg/replacer"
)
const (
// TplNameSponge 模板目录名称
TplNameSponge = "sponge"
)
var (
// 指定文件替换标记
modelFile = "model/userExample.go"
modelFileMark = "// todo generate model codes to here"
daoFile = "dao/userExample.go"
daoFileMark = "// todo generate the update fields code to here"
daoTestFile = "dao/userExample_test.go"
handlerFile = "types/userExample_types.go"
handlerFileMark = "// todo generate the request and response struct to here"
handlerTestFile = "handler/userExample_test.go"
mainFile = "serverNameExample/main.go"
mainFileMark = "// todo generate the code to register http and grpc services here"
protoFile = "v1/userExample.proto"
protoFileMark = "// todo generate the protobuf code here"
serviceTestFile = "service/userExample_test.go"
serviceClientFile = "service/userExample_client_test.go"
serviceFileMark = "// todo generate the service struct code here"
dockerFile = "build/Dockerfile"
dockerFileMark = "# todo generate dockerfile code for http or grpc here"
dockerFileBuild = "build/Dockerfile_build"
dockerFileBuildMark = "# todo generate dockerfile_build code for http or grpc here"
dockerComposeFile = "deployments/docker-compose/docker-compose.yml"
dockerComposeFileMark = "# todo generate docker-compose.yml code for http or grpc here"
k8sDeploymentFile = "deployments/kubernetes/serverNameExample-deployment.yml"
k8sDeploymentFileMark = "# todo generate k8s-deployment.yml code for http or grpc here"
k8sServiceFile = "deployments/kubernetes/serverNameExample-svc.yml"
k8sServiceFileMark = "# todo generate k8s-svc.yml code for http or grpc here"
imageBuildFile = "scripts/image-build.sh"
readmeFile = "sponge/README.md"
// 清除标记的模板代码片段标记
startMark = []byte("// delete the templates code start")
endMark = []byte("// delete the templates code end")
wellStartMark = bytes.ReplaceAll(startMark, []byte("//"), []byte("#"))
wellEndMark = bytes.ReplaceAll(endMark, []byte("//"), []byte("#"))
onlyGrpcStartMark = []byte("// only grpc use start")
onlyGrpcEndMark = []byte("// only grpc use end\n")
wellOnlyGrpcStartMark = bytes.ReplaceAll(onlyGrpcStartMark, []byte("//"), []byte("#"))
wellOnlyGrpcEndMark = bytes.ReplaceAll(onlyGrpcEndMark, []byte("//"), []byte("#"))
selfPackageName = "github.com/zhufuyi/sponge"
)
func adjustmentOfIDType(handlerCodes string) string {
return idTypeToStr(idTypeFixToUint64(handlerCodes))
}
func idTypeFixToUint64(handlerCodes string) string {
subStart := "ByIDRequest struct {"
subEnd := "`" + `json:"id" binding:""` + "`"
if subBytes := gofile.FindSubBytesNotIn([]byte(handlerCodes), []byte(subStart), []byte(subEnd)); len(subBytes) > 0 {
old := subStart + string(subBytes) + subEnd
newStr := subStart + "\n\tID uint64 " + subEnd + " // uint64 id\n"
handlerCodes = strings.ReplaceAll(handlerCodes, old, newStr)
}
return handlerCodes
}
func idTypeToStr(handlerCodes string) string {
subStart := "ByIDRespond struct {"
subEnd := "`" + `json:"id"` + "`"
if subBytes := gofile.FindSubBytesNotIn([]byte(handlerCodes), []byte(subStart), []byte(subEnd)); len(subBytes) > 0 {
old := subStart + string(subBytes) + subEnd
newStr := subStart + "\n\tID string " + subEnd + " // covert to string id\n"
handlerCodes = strings.ReplaceAll(handlerCodes, old, newStr)
}
return handlerCodes
}
func deleteFieldsMark(r replacer.Replacer, filename string, startMark []byte, endMark []byte) []replacer.Field {
var fields []replacer.Field
data, err := r.ReadFile(filename)
if err != nil {
fmt.Printf("read the file '%s' error: %v\n", filename, err)
return fields
}
if subBytes := gofile.FindSubBytes(data, startMark, endMark); len(subBytes) > 0 {
fields = append(fields,
replacer.Field{ // 清除标记的模板代码
Old: string(subBytes),
New: "",
},
)
}
return fields
}
func replaceFileContentMark(r replacer.Replacer, filename string, newContent string) []replacer.Field {
var fields []replacer.Field
data, err := r.ReadFile(filename)
if err != nil {
fmt.Printf("read the file '%s' error: %v\n", filename, err)
return fields
}
fields = append(fields, replacer.Field{
Old: string(data),
New: newContent,
})
return fields
}
// 解析镜像仓库host和name
func parseImageRepoAddr(addr string) (string, string) {
splits := strings.Split(addr, "/")
// 官方仓库地址
if len(splits) == 1 {
return "https://index.docker.io/v1", addr
}
// 非官方仓库地址
l := len(splits)
return strings.Join(splits[:l-1], "/"), splits[l-1]
}

View File

@ -0,0 +1,149 @@
package generate
import (
"fmt"
"os"
"strings"
"github.com/zhufuyi/sponge/pkg/gofile"
"github.com/zhufuyi/sponge/pkg/jy2struct"
"github.com/spf13/cobra"
)
// ConfigCommand covert yaml to struct command
func ConfigCommand() *cobra.Command {
var (
ysArgs = jy2struct.Args{
Tags: "json",
SubStruct: true,
}
serverDir = ""
)
cmd := &cobra.Command{
Use: "config",
Short: "Generate go config code from yaml file",
Long: `generate go config code from yaml file.
Examples:
# generate config code in server directory, the yaml configuration file must be in <yourServerDir>/configs directory.
sponge config --server-dir=/yourServerDir
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
files, err := getYAMLFile(serverDir)
if err != nil {
return err
}
err = runGenConfigCommand(files, ysArgs)
if err != nil {
return err
}
fmt.Println("covert yaml to go struct successfully.")
return nil
},
}
cmd.Flags().StringVarP(&serverDir, "server-dir", "d", "", "server directory")
_ = cmd.MarkFlagRequired("server-dir")
return cmd
}
func runGenConfigCommand(files map[string]configType, ysArgs jy2struct.Args) error {
for outputFile, config := range files {
ysArgs.Format = "yaml"
ysArgs.InputFile = config.configFile
var startCode string
if config.isConfigCenter {
ysArgs.Name = "Center"
startCode = configFileCcCode
} else {
ysArgs.Name = "Config"
startCode = configFileCode
}
structCodes, err := jy2struct.Covert(&ysArgs)
if err != nil {
return err
}
err = saveFile(config.configFile, outputFile, startCode+structCodes)
if err != nil {
return err
}
}
return nil
}
type configType struct {
configFile string
isConfigCenter bool
}
// 从configs目录读取所有yaml文件目录一个是.yml另一个是cc.yml
func getYAMLFile(serverDir string) (map[string]configType, error) {
// 生成目标文件:配置文件
files := make(map[string]configType)
configsDir := serverDir + gofile.GetPathDelimiter() + "configs"
goConfigDir := serverDir + gofile.GetPathDelimiter() + "internal" + gofile.GetPathDelimiter() + "config"
ymlFiles, err := gofile.ListFiles(configsDir, gofile.WithSuffix(".yml"))
if err != nil {
return nil, err
}
if len(ymlFiles) > 2 {
return nil, fmt.Errorf("config files are allowed up to 2, currently there are %d", len(ymlFiles))
}
yamlFiles, err := gofile.ListFiles(configsDir, gofile.WithSuffix(".yaml"))
if err != nil {
return nil, err
}
if len(yamlFiles) > 2 {
return nil, fmt.Errorf("config files are allowed up to 2, currently there are %d", len(yamlFiles))
}
if len(ymlFiles) == 0 && len(yamlFiles) == 0 {
return nil, fmt.Errorf("not found config files in directory %s", configsDir)
}
if len(ymlFiles) != 0 && len(yamlFiles) != 0 {
return nil, fmt.Errorf("please use 'yml' or 'yaml' suffixes for configuration files, do not mix them")
}
if len(ymlFiles) > 0 {
for _, file := range ymlFiles {
name := gofile.GetFilename(file)
files[goConfigDir+gofile.GetPathDelimiter()+strings.ReplaceAll(name, ".yml", ".go")] = configType{
configFile: file,
isConfigCenter: strings.Contains(name, "cc.yml"),
}
}
return files, nil
}
if len(yamlFiles) > 0 {
for _, file := range yamlFiles {
name := gofile.GetFilename(file)
files[goConfigDir+gofile.GetPathDelimiter()+strings.ReplaceAll(name, ".yaml", ".go")] = configType{
configFile: file,
isConfigCenter: strings.Contains(name, "cc.yaml"),
}
}
}
return files, nil
}
func saveFile(inputFile string, outputFile string, code string) error {
err := os.WriteFile(outputFile, []byte(code), 0666)
if err != nil {
return err
}
fmt.Printf("%s ----> %s\n", inputFile, outputFile)
return nil
}

View File

@ -0,0 +1,126 @@
package generate
import (
"errors"
"fmt"
"github.com/zhufuyi/sponge/pkg/replacer"
"github.com/zhufuyi/sponge/pkg/sql2code"
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
"github.com/spf13/cobra"
)
// DaoCommand generate dao code
func DaoCommand() *cobra.Command {
var (
moduleName string // 服务名称,也就是包名称
outPath string // 输出目录
sqlArgs = sql2code.Args{
Package: "model",
JSONTag: true,
GormType: true,
}
)
cmd := &cobra.Command{
Use: "dao",
Short: "Generate dao code",
Long: `generate dao code.
Examples:
# generate dao code and embed 'gorm.model' struct.
sponge dao --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
# generate dao code, structure fields correspond to the column names of the table.
sponge dao --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
# generate dao code and specify the output directory, Note: if the file already exists, code generation will be canceled.
sponge dao --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
codes, err := sql2code.Generate(&sqlArgs)
if err != nil {
return err
}
return runGenDaoCommand(moduleName, codes, outPath)
},
}
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
_ = cmd.MarkFlagRequired("module-name")
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
_ = cmd.MarkFlagRequired("db-dsn")
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
_ = cmd.MarkFlagRequired("db-table")
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./dao_<time>")
return cmd
}
func runGenDaoCommand(moduleName string, codes map[string]string, outPath string) error {
subTplName := "dao"
r := Replacers[TplNameSponge]
if r == nil {
return errors.New("r is nil")
}
// 设置模板信息
subDirs := []string{"internal/model", "internal/cache", "internal/dao"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
ignoreFiles := []string{"init.go", "init_test.go"} // 指定子目录下忽略处理的文件
r.SetSubDirs(subDirs...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreFiles(ignoreFiles...)
fields := addDAOFields(moduleName, r, codes)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(outPath, subTplName)
if err := r.SaveFiles(); err != nil {
return err
}
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
return nil
}
func addDAOFields(moduleName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
var fields []replacer.Field
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
fields = append(fields, []replacer.Field{
{ // 替换model/userExample.go文件内容
Old: modelFileMark,
New: codes[parser.CodeTypeModel],
},
{ // 替换dao/userExample.go文件内容
Old: daoFileMark,
New: codes[parser.CodeTypeDAO],
},
{
Old: selfPackageName + "/" + r.GetSourcePath(),
New: moduleName,
},
{
Old: "github.com/zhufuyi/sponge",
New: moduleName,
},
{
Old: moduleName + "/pkg",
New: "github.com/zhufuyi/sponge/pkg",
},
{
Old: "UserExample",
New: codes[parser.TableName],
IsCaseSensitive: true,
},
}...)
return fields
}

View File

@ -0,0 +1,271 @@
package generate
import (
"errors"
"fmt"
"math/rand"
"strings"
"github.com/zhufuyi/sponge/pkg/gofile"
"github.com/zhufuyi/sponge/pkg/replacer"
"github.com/zhufuyi/sponge/pkg/sql2code"
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
"github.com/huandu/xstrings"
"github.com/spf13/cobra"
)
// GRPCCommand generate grpc code
func GRPCCommand() *cobra.Command {
var (
moduleName string // go.mod文件的module名称
serverName string // 服务名称
projectName string // 项目名称
repoAddr string // 镜像仓库地址
outPath string // 输出目录
sqlArgs = sql2code.Args{
Package: "model",
JSONTag: true,
GormType: true,
}
)
//nolint
cmd := &cobra.Command{
Use: "grpc",
Short: "Generate grpc server code",
Long: `generate grpc server code.
Examples:
# generate grpc server code and embed 'gorm.model' struct.
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
# generate grpc server code, structure fields correspond to the column names of the table.
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
# generate grpc server code and specify the output directory, Note: if the file already exists, code generation will be canceled.
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
# generate grpc server code and specify the docker image repository address.
sponge grpc --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --repo-addr=192.168.3.37:9443/user-name --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
codes, err := sql2code.Generate(&sqlArgs)
if err != nil {
return err
}
return runGenGRPCCommand(moduleName, serverName, projectName, repoAddr, sqlArgs.DBDsn, codes, outPath)
},
}
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
_ = cmd.MarkFlagRequired("module-name")
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
_ = cmd.MarkFlagRequired("server-name")
cmd.Flags().StringVarP(&projectName, "project-name", "p", "", "project name")
_ = cmd.MarkFlagRequired("project-name")
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
_ = cmd.MarkFlagRequired("db-dsn")
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
_ = cmd.MarkFlagRequired("db-table")
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
cmd.Flags().StringVarP(&repoAddr, "repo-addr", "r", "", "docker image repository address, excluding http and repository names")
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./serverName_grpc_<time>")
return cmd
}
func runGenGRPCCommand(moduleName string, serverName string, projectName string, repoAddr string,
dbDSN string, codes map[string]string, outPath string) error {
subTplName := "grpc"
r := Replacers[TplNameSponge]
if r == nil {
return errors.New("replacer is nil")
}
// 设置模板信息
subDirs := []string{} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
ignoreDirs := []string{"cmd/sponge", "sponge/.github", "sponge/.git", "sponge/docs", "sponge/pkg", "sponge/assets",
"sponge/test", "internal/handler", "internal/routers", "internal/types"} // 指定子目录下忽略处理的目录
ignoreFiles := []string{"http_systemCode.go", "http_userExample.go", "http.go", "http_test.go", "http_option.go",
"userExample.pb.go", "userExample.pb.validate.go", "userExample_grpc.pb.go",
"types.pb.go", "types.pb.validate.go", "LICENSE", "doc.go"} // 指定子目录下忽略处理的文件
r.SetSubDirs(subDirs...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreFiles(ignoreFiles...)
fields := addGRPCFields(moduleName, serverName, projectName, repoAddr, r, dbDSN, codes)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(outPath, serverName+"_"+subTplName)
if err := r.SaveFiles(); err != nil {
return err
}
fmt.Printf("generate '%s' project code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
return nil
}
func addGRPCFields(moduleName string, serverName string, projectName string, repoAddr string,
r replacer.Replacer, dbDSN string, codes map[string]string) []replacer.Field {
var fields []replacer.Field
repoHost, _ := parseImageRepoAddr(repoAddr)
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, protoFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, serviceClientFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, serviceTestFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, mainFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, dockerFile, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, dockerFileBuild, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, dockerComposeFile, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, k8sDeploymentFile, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, k8sServiceFile, wellStartMark, wellEndMark)...)
fields = append(fields, replaceFileContentMark(r, readmeFile, "## "+serverName)...)
fields = append(fields, []replacer.Field{
{ // 替换model/userExample.go文件内容
Old: modelFileMark,
New: codes[parser.CodeTypeModel],
},
{ // 替换dao/userExample.go文件内容
Old: daoFileMark,
New: codes[parser.CodeTypeDAO],
},
{ // 替换v1/userExample.proto文件内容
Old: protoFileMark,
New: codes[parser.CodeTypeProto],
},
{ // 替换service/userExample_client_test.go文件内容
Old: serviceFileMark,
New: adjustmentOfIDType(codes[parser.CodeTypeService]),
},
{ // 替换main.go文件内容
Old: mainFileMark,
New: mainFileGrpcCode,
},
{ // 替换Dockerfile文件内容
Old: dockerFileMark,
New: dockerFileGrpcCode,
},
{ // 替换Dockerfile_build文件内容
Old: dockerFileBuildMark,
New: dockerFileBuildGrpcCode,
},
{ // 替换docker-compose.yml文件内容
Old: dockerComposeFileMark,
New: dockerComposeFileGrpcCode,
},
{ // 替换*-deployment.yml文件内容
Old: k8sDeploymentFileMark,
New: k8sDeploymentFileGrpcCode,
},
{ // 替换*-svc.yml文件内容
Old: k8sServiceFileMark,
New: k8sServiceFileGrpcCode,
},
// 替换github.com/zhufuyi/sponge/templates/sponge
{
Old: selfPackageName + "/" + r.GetSourcePath(),
New: moduleName,
},
// 替换目录名称
{
Old: strings.Join([]string{"api", "userExample", "v1"}, gofile.GetPathDelimiter()),
New: strings.Join([]string{"api", serverName, "v1"}, gofile.GetPathDelimiter()),
},
{
Old: "github.com/zhufuyi/sponge",
New: moduleName,
},
{
Old: moduleName + "/pkg",
New: "github.com/zhufuyi/sponge/pkg",
},
{
Old: "api/userExample/v1",
New: fmt.Sprintf("api/%s/v1", serverName),
},
{
Old: "api.userExample.v1",
New: fmt.Sprintf("api.%s.v1", strings.ReplaceAll(serverName, "-", "_")), // proto package 不能存在"-"号
},
{
Old: "sponge api docs",
New: serverName + " api docs",
},
{
Old: "userExampleNO = 1",
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
},
{
Old: "serverNameExample",
New: serverName,
},
// docker镜像和k8s部署脚本替换
{
Old: "server-name-example",
New: xstrings.ToKebabCase(serverName),
},
{
Old: "projectNameExample",
New: projectName,
},
// docker镜像和k8s部署脚本替换
{
Old: "project-name-example",
New: xstrings.ToKebabCase(projectName),
},
{
Old: "repo-addr-example",
New: repoAddr,
},
{
Old: "image-repo-host",
New: repoHost,
},
{
Old: string(onlyGrpcStartMark),
New: "",
},
{
Old: string(onlyGrpcEndMark),
New: "",
},
{
Old: string(wellOnlyGrpcStartMark),
New: "",
},
{
Old: string(wellOnlyGrpcEndMark),
New: "",
},
{
Old: "tmp.go.mod",
New: "go.mod",
},
{
Old: "tmp.gitignore",
New: ".gitignore",
},
{
Old: "tmp.golangci.yml",
New: ".golangci.yml",
},
{
Old: "root:123456@(192.168.3.37:3306)/account",
New: dbDSN,
},
{
Old: "UserExample",
New: codes[parser.TableName],
IsCaseSensitive: true,
},
}...)
return fields
}

View File

@ -0,0 +1,140 @@
package generate
import (
"errors"
"fmt"
"math/rand"
"github.com/zhufuyi/sponge/pkg/replacer"
"github.com/zhufuyi/sponge/pkg/sql2code"
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
"github.com/spf13/cobra"
)
// HandlerCommand generate handler code
func HandlerCommand() *cobra.Command {
var (
moduleName string // go.mod文件的module名称
outPath string // 输出目录
sqlArgs = sql2code.Args{
Package: "model",
JSONTag: true,
GormType: true,
}
)
cmd := &cobra.Command{
Use: "handler",
Short: "Generate handler code",
Long: `generate handler code.
Examples:
# generate handler code and embed 'gorm.model' struct.
sponge handler --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
# generate handler code, structure fields correspond to the column names of the table.
sponge handler --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
# generate handler code and specify the output directory, Note: if the file already exists, code generation will be canceled.
sponge handler --module-name=yourModuleName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
codes, err := sql2code.Generate(&sqlArgs)
if err != nil {
return err
}
return runGenHandlerCommand(moduleName, codes, outPath)
},
}
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
_ = cmd.MarkFlagRequired("module-name")
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
_ = cmd.MarkFlagRequired("db-dsn")
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
_ = cmd.MarkFlagRequired("db-table")
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./handler_<time>")
return cmd
}
func runGenHandlerCommand(moduleName string, codes map[string]string, outPath string) error {
subTplName := "handler"
r := Replacers[TplNameSponge]
if r == nil {
return errors.New("replacer is nil")
}
// 设置模板信息
subDirs := []string{"internal/model", "internal/cache", "internal/dao",
"internal/ecode", "internal/handler", "internal/routers", "internal/types"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
ignoreFiles := []string{"init.go", "init_test.go", "swagger_types.go", "http_systemCode.go",
"grpc_systemCode.go", "grpc_userExample.go", "grpc_systemCode_test.go", "routers.go", "routers_test.go"} // 指定子目录下忽略处理的文件
r.SetSubDirs(subDirs...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreFiles(ignoreFiles...)
fields := addHandlerFields(moduleName, r, codes)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(outPath, subTplName)
if err := r.SaveFiles(); err != nil {
return err
}
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
return nil
}
func addHandlerFields(moduleName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
var fields []replacer.Field
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, handlerFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, handlerTestFile, startMark, endMark)...)
fields = append(fields, []replacer.Field{
{ // 替换model/userExample.go文件内容
Old: modelFileMark,
New: codes[parser.CodeTypeModel],
},
{ // 替换dao/userExample.go文件内容
Old: daoFileMark,
New: codes[parser.CodeTypeDAO],
},
{ // 替换handler/userExample.go文件内容
Old: handlerFileMark,
New: adjustmentOfIDType(codes[parser.CodeTypeHandler]),
},
{
Old: selfPackageName + "/" + r.GetSourcePath(),
New: moduleName,
},
{
Old: "github.com/zhufuyi/sponge",
New: moduleName,
},
{
Old: "userExampleNO = 1",
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
},
{
Old: moduleName + "/pkg",
New: "github.com/zhufuyi/sponge/pkg",
},
{
Old: "UserExample",
New: codes[parser.TableName],
IsCaseSensitive: true,
},
}...)
return fields
}

View File

@ -0,0 +1,237 @@
package generate
import (
"errors"
"fmt"
"math/rand"
"github.com/zhufuyi/sponge/pkg/replacer"
"github.com/zhufuyi/sponge/pkg/sql2code"
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
"github.com/huandu/xstrings"
"github.com/spf13/cobra"
)
// HTTPCommand generate http code
func HTTPCommand() *cobra.Command {
var (
moduleName string // go.mod文件的module名称
serverName string // 服务名称
projectName string // 项目名称
repoAddr string // 镜像仓库地址
outPath string // 输出目录
sqlArgs = sql2code.Args{
Package: "model",
JSONTag: true,
GormType: true,
}
)
//nolint
cmd := &cobra.Command{
Use: "http",
Short: "Generate http server code",
Long: `generate http server code.
Examples:
# generate http code and embed 'gorm.model' struct.
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
# generate http code, structure fields correspond to the column names of the table.
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
# generate http code and specify the output directory, Note: if the file already exists, code generation will be canceled.
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
# generate http code and specify the docker image repository address.
sponge http --module-name=yourModuleName --server-name=yourServerName --project-name=yourProjectName --repo-addr=192.168.3.37:9443/user-name --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
codes, err := sql2code.Generate(&sqlArgs)
if err != nil {
return err
}
return runGenHTTPCommand(moduleName, serverName, projectName, repoAddr, sqlArgs.DBDsn, codes, outPath)
},
}
cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
_ = cmd.MarkFlagRequired("module-name")
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
_ = cmd.MarkFlagRequired("server-name")
cmd.Flags().StringVarP(&projectName, "project-name", "p", "", "project name")
_ = cmd.MarkFlagRequired("project-name")
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
_ = cmd.MarkFlagRequired("db-dsn")
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
_ = cmd.MarkFlagRequired("db-table")
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
cmd.Flags().StringVarP(&repoAddr, "repo-addr", "r", "", "docker image repository address, excluding http and repository names")
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./serverName_http_<time>")
return cmd
}
func runGenHTTPCommand(moduleName string, serverName string, projectName string, repoAddr string,
dbDSN string, codes map[string]string, outPath string) error {
subTplName := "http"
r := Replacers[TplNameSponge]
if r == nil {
return errors.New("replacer is nil")
}
// 设置模板信息
subDirs := []string{} // 只处理的子目录,如果为空或者没有指定的子目录,表示所有文件
ignoreDirs := []string{"cmd/sponge", "sponge/.github", "sponge/.git", "sponge/api", "sponge/pkg", "sponge/assets",
"sponge/test", "sponge/third_party", "internal/service"} // 指定子目录下忽略处理的目录
ignoreFiles := []string{"swagger.json", "swagger.yaml", "proto.html", "protoc.sh",
"proto-doc.sh", "grpc.go", "grpc_option.go", "grpc_test.go", "LICENSE", "doc.go",
"grpc_userExample.go", "grpc_systemCode.go", "grpc_systemCode_test.go"} // 指定子目录下忽略处理的文件
r.SetSubDirs(subDirs...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreFiles(ignoreFiles...)
fields := addHTTPFields(moduleName, serverName, projectName, repoAddr, r, dbDSN, codes)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(outPath, serverName+"_"+subTplName)
if err := r.SaveFiles(); err != nil {
return err
}
fmt.Printf("generate '%s' project code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
return nil
}
func addHTTPFields(moduleName string, serverName string, projectName string, repoAddr string,
r replacer.Replacer, dbDSN string, codes map[string]string) []replacer.Field {
var fields []replacer.Field
repoHost, _ := parseImageRepoAddr(repoAddr)
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, handlerFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, handlerTestFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, mainFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, mainFile, onlyGrpcStartMark, onlyGrpcEndMark)...)
fields = append(fields, deleteFieldsMark(r, dockerFile, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, dockerFileBuild, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, dockerComposeFile, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, k8sDeploymentFile, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, k8sServiceFile, wellStartMark, wellEndMark)...)
fields = append(fields, deleteFieldsMark(r, imageBuildFile, wellOnlyGrpcStartMark, wellOnlyGrpcEndMark)...)
fields = append(fields, replaceFileContentMark(r, readmeFile, "## "+serverName)...)
fields = append(fields, []replacer.Field{
{ // 替换model/userExample.go文件内容
Old: modelFileMark,
New: codes[parser.CodeTypeModel],
},
{ // 替换dao/userExample.go文件内容
Old: daoFileMark,
New: codes[parser.CodeTypeDAO],
},
{ // 替换handler/userExample.go文件内容
Old: handlerFileMark,
New: adjustmentOfIDType(codes[parser.CodeTypeHandler]),
},
{ // 替换main.go文件内容
Old: mainFileMark,
New: mainFileHTTPCode,
},
{ // 替换Dockerfile文件内容
Old: dockerFileMark,
New: dockerFileHTTPCode,
},
{ // 替换Dockerfile_build文件内容
Old: dockerFileBuildMark,
New: dockerFileBuildHTTPCode,
},
{ // 替换docker-compose.yml文件内容
Old: dockerComposeFileMark,
New: dockerComposeFileHTTPCode,
},
{ // 替换*-deployment.yml文件内容
Old: k8sDeploymentFileMark,
New: k8sDeploymentFileHTTPCode,
},
{ // 替换*-svc.yml文件内容
Old: k8sServiceFileMark,
New: k8sServiceFileHTTPCode,
},
// 替换github.com/zhufuyi/sponge/templates/sponge
{
Old: selfPackageName + "/" + r.GetSourcePath(),
New: moduleName,
},
{
Old: "github.com/zhufuyi/sponge",
New: moduleName,
},
{
Old: moduleName + "/pkg",
New: "github.com/zhufuyi/sponge/pkg",
},
{
Old: "sponge api docs",
New: serverName + " api docs",
},
{
Old: "userExampleNO = 1",
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
},
{
Old: "serverNameExample",
New: serverName,
},
// docker镜像和k8s部署脚本替换
{
Old: "server-name-example",
New: xstrings.ToKebabCase(serverName),
},
{
Old: "projectNameExample",
New: projectName,
},
// docker镜像和k8s部署脚本替换
{
Old: "project-name-example",
New: xstrings.ToKebabCase(projectName),
},
{
Old: "repo-addr-example",
New: repoAddr,
},
{
Old: "image-repo-host",
New: repoHost,
},
{
Old: "tmp.go.mod",
New: "go.mod",
},
{
Old: "tmp.gitignore",
New: ".gitignore",
},
{
Old: "tmp.golangci.yml",
New: ".golangci.yml",
},
{
Old: "root:123456@(192.168.3.37:3306)/account",
New: dbDSN,
},
{
Old: "UserExample",
New: codes[parser.TableName],
IsCaseSensitive: true,
},
}...)
return fields
}

View File

@ -0,0 +1,77 @@
package generate
import (
"embed"
"fmt"
"os"
"strings"
"github.com/zhufuyi/sponge/pkg/gofile"
"github.com/zhufuyi/sponge/pkg/replacer"
)
// Replacers 模板名称对应的接口
var Replacers = map[string]replacer.Replacer{}
// Template 模板信息
type Template struct {
Name string
FS embed.FS
FilePath string
}
// Init 初始化模板
func Init(name string, filepath string) error {
// 判断模板文件是否存在,不存在,提示先更新
if !gofile.IsExists(filepath) {
if isShowCommand() {
return nil
}
return fmt.Errorf(`Hint: for the first time, run the command "sponge update"`)
}
var err error
if _, ok := Replacers[name]; ok {
panic(fmt.Sprintf("template name '%s' already exists", name))
}
Replacers[name], err = replacer.New(filepath)
if err != nil {
return err
}
return nil
}
// InitFS 初始化FS模板
func InitFS(name string, filepath string, fs embed.FS) {
var err error
if _, ok := Replacers[name]; ok {
panic(fmt.Sprintf("template name '%s' already exists", name))
}
Replacers[name], err = replacer.NewFS(filepath, fs)
if err != nil {
panic(err)
}
}
func isShowCommand() bool {
l := len(os.Args)
// sponge
if l == 1 {
return true
}
// sponge update or sponge -h
if l == 2 {
if os.Args[1] == "update" || os.Args[1] == "-h" {
return true
}
return false
}
if l > 2 {
return strings.Contains(strings.Join(os.Args[:3], ""), "update")
}
return false
}

View File

@ -0,0 +1,105 @@
package generate
import (
"errors"
"fmt"
"github.com/zhufuyi/sponge/pkg/replacer"
"github.com/zhufuyi/sponge/pkg/sql2code"
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
"github.com/spf13/cobra"
)
// ModelCommand generate model code
func ModelCommand() *cobra.Command {
var (
outPath string // 输出目录
sqlArgs = sql2code.Args{
Package: "model",
JSONTag: true,
GormType: true,
}
)
cmd := &cobra.Command{
Use: "model",
Short: "Generate model code",
Long: `generate model code.
Examples:
# generate model code and embed 'gorm.Model' struct.
sponge model --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
# generate model code, structure fields correspond to the column names of the table.
sponge model --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
# generate model code and specify the output directory, Note: if the file already exists, code generation will be canceled.
sponge model --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
codes, err := sql2code.Generate(&sqlArgs)
if err != nil {
return err
}
return runGenModelCommand(codes, outPath)
},
}
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
_ = cmd.MarkFlagRequired("db-dsn")
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
_ = cmd.MarkFlagRequired("db-table")
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./model_<time>")
return cmd
}
func runGenModelCommand(codes map[string]string, outPath string) error {
subTplName := "model"
r := Replacers[TplNameSponge]
if r == nil {
return errors.New("replacer is nil")
}
// 设置模板信息
subDirs := []string{"internal/model"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
ignoreFiles := []string{"init.go", "init_test.go"} // 指定子目录下忽略处理的文件
r.SetSubDirs(subDirs...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreFiles(ignoreFiles...)
fields := addModelFields(r, codes)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(outPath, subTplName)
if err := r.SaveFiles(); err != nil {
return err
}
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
return nil
}
func addModelFields(r replacer.Replacer, codes map[string]string) []replacer.Field {
var fields []replacer.Field
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
fields = append(fields, []replacer.Field{
{ // 替换model/userExample.go文件内容
Old: modelFileMark,
New: codes[parser.CodeTypeModel],
},
{
Old: "UserExample",
New: codes[parser.TableName],
IsCaseSensitive: true,
},
}...)
return fields
}

View File

@ -0,0 +1,127 @@
package generate
import (
"errors"
"fmt"
"strings"
"github.com/zhufuyi/sponge/pkg/gofile"
"github.com/zhufuyi/sponge/pkg/replacer"
"github.com/zhufuyi/sponge/pkg/sql2code"
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
"github.com/spf13/cobra"
)
// ProtoCommand generate protobuf code
func ProtoCommand() *cobra.Command {
var (
moduleName string // go.mod文件的module名称
serverName string // 服务名称
outPath string // 输出目录
sqlArgs = sql2code.Args{}
)
cmd := &cobra.Command{
Use: "proto",
Short: "Generate protobuf code",
Long: `generate protobuf code.
Examples:
# generate protobuf code.
sponge proto --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
# generate protobuf code and specify the output directory, Note: if the file already exists, code generation will be canceled.
sponge proto --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
codes, err := sql2code.Generate(&sqlArgs)
if err != nil {
return err
}
return runGenProtoCommand(moduleName, serverName, codes, outPath)
},
}
cmd.Flags().StringVarP(&moduleName, "module-name", "p", "", "module-name is the name of the module in the 'go.mod' file")
_ = cmd.MarkFlagRequired("module-name")
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
_ = cmd.MarkFlagRequired("server-name")
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
_ = cmd.MarkFlagRequired("db-dsn")
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
_ = cmd.MarkFlagRequired("db-table")
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./proto_<time>")
return cmd
}
func runGenProtoCommand(moduleName string, serverName string, codes map[string]string, outPath string) error {
subTplName := "proto"
r := Replacers[TplNameSponge]
if r == nil {
return errors.New("replacer is nil")
}
if serverName == "" {
serverName = moduleName
}
// 设置模板信息
subDirs := []string{"api/serverNameExample"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
ignoreFiles := []string{"userExample.pb.go", "userExample.pb.validate.go",
"userExample_grpc.pb.go"} // 指定子目录下忽略处理的文件
r.SetSubDirs(subDirs...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreFiles(ignoreFiles...)
fields := addProtoFields(moduleName, serverName, r, codes)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(outPath, subTplName)
if err := r.SaveFiles(); err != nil {
return err
}
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
return nil
}
func addProtoFields(moduleName string, serverName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
var fields []replacer.Field
fields = append(fields, deleteFieldsMark(r, protoFile, startMark, endMark)...)
fields = append(fields, []replacer.Field{
{ // 替换v1/userExample.proto文件内容
Old: protoFileMark,
New: codes[parser.CodeTypeProto],
},
{
Old: "github.com/zhufuyi/sponge",
New: moduleName,
},
// 替换目录名称
{
Old: strings.Join([]string{"api", "serverNameExample", "v1"}, gofile.GetPathDelimiter()),
New: strings.Join([]string{"api", serverName, "v1"}, gofile.GetPathDelimiter()),
},
{
Old: "api/serverNameExample/v1",
New: fmt.Sprintf("api/%s/v1", serverName),
},
{
Old: "api.serverNameExample.v1",
New: fmt.Sprintf("api.%s.v1", strings.ReplaceAll(serverName, "-", "_")), // proto package 不能存在"-"号
},
{
Old: "UserExample",
New: codes[parser.TableName],
IsCaseSensitive: true,
},
}...)
return fields
}

View File

@ -0,0 +1,167 @@
package generate
import (
"errors"
"fmt"
"math/rand"
"strings"
"github.com/zhufuyi/sponge/pkg/gofile"
"github.com/zhufuyi/sponge/pkg/replacer"
"github.com/zhufuyi/sponge/pkg/sql2code"
"github.com/zhufuyi/sponge/pkg/sql2code/parser"
"github.com/spf13/cobra"
)
// ServiceCommand generate service code
func ServiceCommand() *cobra.Command {
var (
moduleName string // go.mod文件的module名称
serverName string // 服务名称
outPath string // 输出目录
sqlArgs = sql2code.Args{
Package: "model",
JSONTag: true,
GormType: true,
}
)
cmd := &cobra.Command{
Use: "service",
Short: "Generate grpc service code",
Long: `generate grpc service code.
Examples:
# generate service code and embed 'gorm.model' struct.
sponge service --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user
# generate service code, structure fields correspond to the column names of the table.
sponge service --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --embed=false
# generate service code and specify the output directory, Note: if the file already exists, code generation will be canceled.
sponge service --module-name=yourModuleName --server-name=yourServerName --db-dsn=root:123456@(192.168.3.37:3306)/test --db-table=user --out=./yourServerDir
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
codes, err := sql2code.Generate(&sqlArgs)
if err != nil {
return err
}
return runGenServiceCommand(moduleName, serverName, codes, outPath)
},
}
cmd.Flags().StringVarP(&moduleName, "module-name", "p", "", "module-name is the name of the module in the 'go.mod' file")
_ = cmd.MarkFlagRequired("module-name")
cmd.Flags().StringVarP(&serverName, "server-name", "s", "", "server name")
_ = cmd.MarkFlagRequired("server-name")
cmd.Flags().StringVarP(&sqlArgs.DBDsn, "db-dsn", "d", "", "db content addr, e.g. user:password@(host:port)/database")
_ = cmd.MarkFlagRequired("db-dsn")
cmd.Flags().StringVarP(&sqlArgs.DBTable, "db-table", "t", "", "table name")
_ = cmd.MarkFlagRequired("db-table")
cmd.Flags().BoolVarP(&sqlArgs.IsEmbed, "embed", "e", true, "whether to embed 'gorm.Model' struct")
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./service_<time>")
return cmd
}
func runGenServiceCommand(moduleName string, serverName string, codes map[string]string, outPath string) error {
subTplName := "service"
r := Replacers[TplNameSponge]
if r == nil {
return errors.New("replacer is nil")
}
if serverName == "" {
serverName = moduleName
}
// 设置模板信息
subDirs := []string{"internal/model", "internal/cache", "internal/dao",
"internal/ecode", "internal/service", "api/serverNameExample"} // 只处理的指定子目录,如果为空或者没有指定的子目录,表示所有文件
ignoreDirs := []string{} // 指定子目录下忽略处理的目录
ignoreFiles := []string{"init.go", "init_test.go", "http_systemCode.go", "http_userExample.go",
"grpc_systemCode.go", "grpc_systemCode_test.go", "service.go", "service_test.go", "userExample.pb.go",
"userExample.pb.validate.go", "userExample_grpc.pb.go"} // 指定子目录下忽略处理的文件
r.SetSubDirs(subDirs...)
r.SetIgnoreSubDirs(ignoreDirs...)
r.SetIgnoreFiles(ignoreFiles...)
fields := addServiceFields(moduleName, serverName, r, codes)
r.SetReplacementFields(fields)
_ = r.SetOutputDir(outPath, subTplName)
if err := r.SaveFiles(); err != nil {
return err
}
fmt.Printf("generate '%s' code successfully, out = %s\n\n", subTplName, r.GetOutputDir())
return nil
}
func addServiceFields(moduleName string, serverName string, r replacer.Replacer, codes map[string]string) []replacer.Field {
var fields []replacer.Field
fields = append(fields, deleteFieldsMark(r, modelFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, daoTestFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, protoFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, serviceClientFile, startMark, endMark)...)
fields = append(fields, deleteFieldsMark(r, serviceTestFile, startMark, endMark)...)
fields = append(fields, []replacer.Field{
{ // 替换model/userExample.go文件内容
Old: modelFileMark,
New: codes[parser.CodeTypeModel],
},
{ // 替换dao/userExample.go文件内容
Old: daoFileMark,
New: codes[parser.CodeTypeDAO],
},
{ // 替换v1/userExample.proto文件内容
Old: protoFileMark,
New: codes[parser.CodeTypeProto],
},
{ // 替换service/userExample_client_test.go文件内容
Old: serviceFileMark,
New: adjustmentOfIDType(codes[parser.CodeTypeService]),
},
{
Old: selfPackageName + "/" + r.GetSourcePath(),
New: moduleName,
},
{
Old: "github.com/zhufuyi/sponge",
New: moduleName,
},
// 替换目录名称
{
Old: strings.Join([]string{"api", "serverNameExample", "v1"}, gofile.GetPathDelimiter()),
New: strings.Join([]string{"api", serverName, "v1"}, gofile.GetPathDelimiter()),
},
{
Old: "api/serverNameExample/v1",
New: fmt.Sprintf("api/%s/v1", serverName),
},
{
Old: "api.serverNameExample.v1",
New: fmt.Sprintf("api.%s.v1", strings.ReplaceAll(serverName, "-", "_")), // proto package 不能存在"-"号
},
{
Old: "userExampleNO = 1",
New: fmt.Sprintf("userExampleNO = %d", rand.Intn(1000)),
},
{
Old: moduleName + "/pkg",
New: "github.com/zhufuyi/sponge/pkg",
},
{
Old: "UserExample",
New: codes[parser.TableName],
IsCaseSensitive: true,
},
}...)
return fields
}

View File

@ -0,0 +1,204 @@
package generate
const (
mainFileHTTPCode = `func registerServers() []app.IServer {
var servers []app.IServer
// 创建http服务
httpAddr := ":" + strconv.Itoa(config.Get().HTTP.Port)
httpServer := server.NewHTTPServer(httpAddr,
server.WithHTTPReadTimeout(time.Second*time.Duration(config.Get().HTTP.ReadTimeout)),
server.WithHTTPWriteTimeout(time.Second*time.Duration(config.Get().HTTP.WriteTimeout)),
server.WithHTTPIsProd(config.Get().App.Env == "prod"),
)
servers = append(servers, httpServer)
return servers
}`
mainFileGrpcCode = `func registerServers() []app.IServer {
var servers []app.IServer
// 创建grpc服务
grpcAddr := ":" + strconv.Itoa(config.Get().Grpc.Port)
grpcServer := server.NewGRPCServer(grpcAddr, grpcOptions()...)
servers = append(servers, grpcServer)
return servers
}
func grpcOptions() []server.GRPCOption {
var opts []server.GRPCOption
if config.Get().App.EnableRegistryDiscovery {
iRegistry, instance := getETCDRegistry(
config.Get().Etcd.Addrs,
config.Get().App.Name,
[]string{fmt.Sprintf("grpc://%s:%d", config.Get().App.Host, config.Get().Grpc.Port)},
)
opts = append(opts, server.WithRegistry(iRegistry, instance))
}
return opts
}
func getETCDRegistry(etcdEndpoints []string, instanceName string, instanceEndpoints []string) (registry.Registry, *registry.ServiceInstance) {
serviceInstance := registry.NewServiceInstance(instanceName, instanceEndpoints)
cli, err := clientv3.New(clientv3.Config{
Endpoints: etcdEndpoints,
DialTimeout: 5 * time.Second,
DialOptions: []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
},
})
if err != nil {
panic(err)
}
iRegistry := etcd.New(cli)
return iRegistry, serviceInstance
}`
dockerFileHTTPCode = `# 添加curl用在http服务的检查如果用部署在k8s可以不用安装
RUN apk add curl
COPY configs/ /app/configs/
COPY serverNameExample /app/serverNameExample
RUN chmod +x /app/serverNameExample
# http端口
EXPOSE 8080`
dockerFileGrpcCode = `# 添加grpc_health_probe用在grpc服务的健康检查
COPY grpc_health_probe /bin/grpc_health_probe
RUN chmod +x /bin/grpc_health_probe
COPY configs/ /app/configs/
COPY serverNameExample /app/serverNameExample
RUN chmod +x /app/serverNameExample`
dockerFileBuildHTTPCode = `# 添加curl用在http服务的检查如果用部署在k8s可以不用安装
RUN apk add curl
COPY --from=build /serverNameExample /app/serverNameExample
COPY --from=build /go/src/serverNameExample/configs/serverNameExample.yml /app/configs/serverNameExample.yml
# http端口
EXPOSE 8080`
dockerFileBuildGrpcCode = `# 添加grpc_health_probe用在grpc服务的健康检查
COPY --from=build /grpc_health_probe /bin/grpc_health_probe
COPY --from=build /serverNameExample /app/serverNameExample
COPY --from=build /go/src/serverNameExample/configs/serverNameExample.yml /app/configs/serverNameExample.yml`
dockerComposeFileHTTPCode = ` ports:
- "8080:8080" # http端口
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"] # http健康检查镜像必须包含curl命令`
dockerComposeFileGrpcCode = `
ports:
- "8282:8282" # grpc服务端口
- "9082:9082" # grpc metrics端口
healthcheck:
test: ["CMD", "grpc_health_probe", "-addr=localhost:8282"] # grpc健康检查镜像必须包含grpc_health_probe命令`
k8sDeploymentFileHTTPCode = `
ports:
- name: http-port
containerPort: 8080
readinessProbe:
httpGet:
port: http-port
path: /health
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
livenessProbe:
httpGet:
port: http-port
path: /health`
k8sDeploymentFileGrpcCode = `
ports:
- name: grpc-port
containerPort: 8282
- name: metrics-port
containerPort: 9082
readinessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:8282"]
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
livenessProbe:
exec:
command: ["/bin/grpc_health_probe", "-addr=:8282"]`
k8sServiceFileHTTPCode = ` ports:
- name: server-name-example-svc-http-port
port: 8080
targetPort: 8080`
k8sServiceFileGrpcCode = ` ports:
- name: server-name-example-svc-grpc-port
port: 8282
targetPort: 8282
- name: server-name-example-svc-grpc-metrics-port
port: 9082
targetPort: 9082`
configFileCode = `// nolint
// code generated by sponge.
package config
import (
"github.com/zhufuyi/sponge/pkg/conf"
)
var config *Config
func Init(configFile string, fs ...func()) error {
config = &Config{}
return conf.Parse(configFile, config, fs...)
}
func Show() string {
return conf.Show(config)
}
func Get() *Config {
if config == nil {
panic("config is nil")
}
return config
}
func Set(conf *Config) {
config = conf
}
`
configFileCcCode = `// nolint
// code generated by sponge.
package config
import (
"github.com/zhufuyi/sponge/pkg/conf"
)
func NewCenter(configFile string) (*Center, error) {
nacosConf := &Center{}
err := conf.Parse(configFile, nacosConf)
return nacosConf, err
}
`
)

View File

@ -0,0 +1,34 @@
package commands
import (
"github.com/zhufuyi/sponge/cmd/sponge/commands/generate"
"github.com/spf13/cobra"
)
// Version 命令版本号
const Version = "0.0.0"
// NewRootCMD 命令入口
func NewRootCMD() *cobra.Command {
cmd := &cobra.Command{
Use: "sponge",
Long: "sponge management tools",
SilenceErrors: true,
SilenceUsage: true,
Version: Version,
}
cmd.AddCommand(
generate.ModelCommand(),
generate.DaoCommand(),
generate.HandlerCommand(),
generate.HTTPCommand(),
generate.ProtoCommand(),
generate.ServiceCommand(),
generate.GRPCCommand(),
generate.ConfigCommand(),
UpdateCommand(),
)
return cmd
}

View File

@ -0,0 +1,113 @@
package commands
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/zhufuyi/sponge/pkg/gobash"
"github.com/zhufuyi/sponge/pkg/gofile"
"github.com/spf13/cobra"
)
// UpdateCommand update sponge binaries
func UpdateCommand() *cobra.Command {
var executor string
var enableCNGoProxy bool
cmd := &cobra.Command{
Use: "update",
Short: "Update sponge to the latest version",
Long: `update sponge to the latest version.
Examples:
# for linux
sponge update
# for windows, need to specify the bash file
sponge update --executor="D:\Program Files\cmder\vendor\git-for-windows\bin\bash.exe"
# use https://goproxy.cn goproxy
sponge update -g
`,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if executor != "" {
gobash.SetExecutorPath(executor)
}
err := runUpdateCommand(enableCNGoProxy)
if err != nil {
return err
}
err = CopyToTempDir()
if err != nil {
return err
}
fmt.Println("update sponge successfully.")
return nil
},
}
cmd.Flags().StringVarP(&executor, "executor", "e", "", "for windows systems, you need to specify the bash executor path.")
cmd.Flags().BoolVarP(&enableCNGoProxy, "enable-cn-goproxy", "g", false, "is $GOPROXY turn on 'https://goproxy.cn'")
return cmd
}
func runUpdateCommand(enableCNGoProxy bool) error {
fmt.Println("update sponge ......")
ctx, _ := context.WithTimeout(context.Background(), time.Minute*5) //nolint
command := "go install github.com/zhufuyi/sponge/cmd/sponge@latest"
if enableCNGoProxy {
command = "GOPROXY=https://goproxy.cn,direct && " + command
}
result := gobash.Run(ctx, command)
for v := range result.StdOut {
fmt.Printf("%s", v)
}
if result.Err != nil {
return fmt.Errorf("exec command failed, %v", result.Err)
}
return nil
}
// CopyToTempDir 复制模板文件到临时目录下
func CopyToTempDir() error {
result, err := gobash.Exec("go env GOPATH")
if err != nil {
return fmt.Errorf("Exec() error %v", err)
}
gopath := strings.ReplaceAll(string(result), "\n", "")
if gopath == "" {
return fmt.Errorf("$GOPATH is empty, you need set $GOPATH in your $PATH")
}
// 找出新版本sponge代码文件夹
command := "ls $(go env GOPATH)/pkg/mod/github.com/zhufuyi | grep sponge@ | sort -r | head -1"
result, err = gobash.Exec(command)
if err != nil {
return fmt.Errorf("Exec() error %v", err)
}
latestSpongeDirName := strings.ReplaceAll(string(result), "\n", "")
if latestSpongeDirName == "" {
return fmt.Errorf("not found 'sponge' directory in '$GOPATH/pkg/mod/github.com/zhufuyi'")
}
srcDir := fmt.Sprintf("%s/pkg/mod/github.com/zhufuyi/%s", gopath, latestSpongeDirName)
destDir := os.TempDir() + "/sponge"
// 复制到临时目录
_ = os.RemoveAll(adaptPathDelimiter(destDir))
command = fmt.Sprintf(`cp -rf %s %s`, adaptPathDelimiter(srcDir), adaptPathDelimiter(destDir))
_, err = gobash.Exec(command)
return err
}
func adaptPathDelimiter(filePath string) string {
if gofile.IsWindows() {
filePath = strings.ReplaceAll(filePath, "\\", "/")
}
return filePath
}

37
cmd/sponge/main.go Normal file
View File

@ -0,0 +1,37 @@
package main
import (
"fmt"
"math/rand"
"os"
"time"
"github.com/zhufuyi/sponge/cmd/sponge/commands"
"github.com/zhufuyi/sponge/cmd/sponge/commands/generate"
"github.com/zhufuyi/sponge/pkg/gofile"
)
/*
//go:embed templates/sponge
var microServiceFS embed.FS
*/
func main() {
rand.Seed(time.Now().UnixNano())
// 初始化模板,执行命令需要依赖真实文件
err := generate.Init(generate.TplNameSponge, os.TempDir()+gofile.GetPathDelimiter()+"sponge")
if err != nil {
fmt.Println(err)
return
}
// 初始FS化模板执行命令不需要依赖文件
//replacer.InitFS(gen.TplNameSponge, "templates/sponge", microServiceFS)
rootCMD := commands.NewRootCMD()
if err := rootCMD.Execute(); err != nil {
rootCMD.PrintErrln("Error:", err)
os.Exit(1)
}
}

View File

@ -4,12 +4,13 @@
# app 设置
app:
name: "serverNameExample" # 服务名称
env: "dev" # 运行环境dev:开发环境prod:生产环境,pre:预生产环境
env: "dev" # 运行环境dev:开发环境prod:生产环境,test:测试环境
version: "v0.0.0" # 版本
host: "127.0.0.1" # 主机名称或ip
enableProfile: false # 是否开启性能分析功能true:开启false:关闭
enableMetrics: true # 是否开启指标采集true:开启false:关闭
enableLimit: false # 是否开启限流true:开启false:关闭
enableCircuitBreaker: false # 是否开启熔断true:开启false:关闭
enableTracing: false # 是否开启链路跟踪true:开启false:关闭
enableRegistryDiscovery: false # 是否开启注册与发现true:开启false:关闭
@ -34,12 +35,6 @@ logger:
level: "info" # 输出日志级别 debug, info, warn, error默认是debug
format: "console" # 输出格式console或json默认是console
isSave: false # false:输出到终端true:输出到文件默认是false
logFileConfig: # isSave=true时有效
filename: "out.log" # 文件名称默认值out.log
maxSize: 20 # 最大文件大小(MB)默认值10MB
maxBackups: 50 # 保留旧文件的最大个数默认值100个
maxAge: 15 # 保留旧文件的最大天数默认值30天
isCompression: true # 是否压缩/归档旧文件默认值false
# mysql 设置
@ -72,10 +67,3 @@ jaeger:
# etcd 配置
etcd:
addrs: ["192.168.3.37:2379"]
# limit 配置
rateLimiter:
dimension: "path" # 限流维度支持path和ip两种默认是path
qpsLimit: 1000 # 持续每秒允许成功请求数默认是500
maxLimit: 2000 # 瞬时最大允许峰值默认是1000通常大于qpsLimit

11
doc.go
View File

@ -1,8 +1,6 @@
// Package sponge is a go microservices framework, a tool for quickly creating complete microservices code for http or grpc.
// Generate `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` code from the SQL DDL,
// which can be combined into full services(similar to how a broken sponge cell automatically reorganises itself into a new sponge).
//
// combined with the [sponge](https://github.com/zhufuyi/sponge@sponge) tool to generate framework code。
// Package sponge is a microservices framework for quickly creating http or grpc code.
// Generate codes `config`, `ecode`, `model`, `dao`, `handler`, `router`, `http`, `proto`, `service`, `grpc` from the SQL DDL,
// these codes can be combined into complete services (similar to how a broken sponge cell can automatically reorganize into a new sponge).
//
// sponge -h
// sponge management tools
@ -12,7 +10,7 @@
//
// Available Commands:
// completion Generate the autocompletion script for the specified shell
// config Generate go config code
// config Generate go config code from yaml file
// dao Generate dao code
// grpc Generate grpc server code
// handler Generate handler code
@ -21,4 +19,5 @@
// model Generate model code
// proto Generate protobuf code
// service Generate grpc service code
// update Update sponge to the latest version
package sponge

View File

@ -187,7 +187,7 @@ const docTemplate = `{
},
"/api/v1/userExamples/ids": {
"post": {
"description": "使用post请求根据id数组获取userExample列表",
"description": "使用post请求根据多个id获取userExample列表",
"consumes": [
"application/json"
],
@ -197,7 +197,7 @@ const docTemplate = `{
"tags": [
"userExample"
],
"summary": "根据id数组获取userExample列表",
"summary": "根据多个id获取userExample列表",
"parameters": [
{
"description": "id 数组",
@ -332,6 +332,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"ids": {
"description": "id列表",
"type": "array",
"minItems": 1,
"items": {

View File

@ -183,7 +183,7 @@
},
"/api/v1/userExamples/ids": {
"post": {
"description": "使用post请求根据id数组获取userExample列表",
"description": "使用post请求根据多个id获取userExample列表",
"consumes": [
"application/json"
],
@ -193,7 +193,7 @@
"tags": [
"userExample"
],
"summary": "根据id数组获取userExample列表",
"summary": "根据多个id获取userExample列表",
"parameters": [
{
"description": "id 数组",
@ -328,6 +328,7 @@
"type": "object",
"properties": {
"ids": {
"description": "id列表",
"type": "array",
"minItems": 1,
"items": {

View File

@ -51,6 +51,7 @@ definitions:
types.GetUserExamplesByIDsRequest:
properties:
ids:
description: id列表
items:
type: integer
minItems: 1
@ -234,7 +235,7 @@ paths:
post:
consumes:
- application/json
description: 使用post请求根据id数组获取userExample列表
description: 使用post请求根据多个id获取userExample列表
parameters:
- description: id 数组
in: body
@ -249,7 +250,7 @@ paths:
description: OK
schema:
$ref: '#/definitions/types.Result'
summary: 根据id数组获取userExample列表
summary: 根据多个id获取userExample列表
tags:
- userExample
/health:

30
go.mod
View File

@ -4,8 +4,8 @@ go 1.19
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
github.com/alicebob/miniredis/v2 v2.23.0
github.com/blastrain/vitess-sqlparser v0.0.0-20201030050434-a139afbb1aba
github.com/bojand/ghz v0.110.0
github.com/dgraph-io/ristretto v0.1.0
github.com/envoyproxy/protoc-gen-validate v0.6.2
@ -13,23 +13,25 @@ require (
github.com/gin-contrib/cors v1.3.1
github.com/gin-contrib/pprof v1.4.0
github.com/gin-gonic/gin v1.8.1
github.com/go-kratos/aegis v0.1.3
github.com/go-playground/validator/v10 v10.11.0
github.com/go-redis/redis/extra/redisotel v0.3.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2
github.com/golang/snappy v0.0.3
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/gunerhuseyin/goprometheus v0.0.2
github.com/hashicorp/consul/api v1.12.0
github.com/huandu/xstrings v1.3.1
github.com/jinzhu/copier v0.3.5
github.com/jinzhu/inflection v1.0.0
github.com/nacos-group/nacos-sdk-go/v2 v2.1.0
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/prometheus/client_golang v1.13.0
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad
github.com/spf13/cast v1.5.0
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
@ -47,9 +49,9 @@ require (
go.opentelemetry.io/otel/trace v1.9.0
go.uber.org/zap v1.21.0
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
google.golang.org/grpc v1.48.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.3.5
gorm.io/gorm v1.23.8
)
@ -57,21 +59,19 @@ require (
require (
cloud.google.com/go/compute v1.6.1 // indirect
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
github.com/armon/go-metrics v0.3.10 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cactus/go-statsd-client/statsd v0.0.0-00010101000000-000000000000 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
@ -86,6 +86,7 @@ require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
@ -93,7 +94,6 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-redis/redis/extra/rediscmd v0.2.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
@ -107,13 +107,14 @@ require (
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/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jhump/protoreflect v1.9.0 // indirect
github.com/jinzhu/configor v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
@ -133,13 +134,14 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/shirou/gopsutil/v3 v3.21.8 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.9 // indirect
github.com/tklauser/numcpus v0.3.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.1.15 // indirect
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
@ -153,15 +155,11 @@ require (
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.9.0
replace github.com/cactus/go-statsd-client/statsd => github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c

63
go.sum
View File

@ -61,8 +61,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q=
github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@ -71,15 +69,13 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
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/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/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -108,12 +104,12 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/blastrain/vitess-sqlparser v0.0.0-20201030050434-a139afbb1aba h1:hBK2BWzm0OzYZrZy9yzvZZw59C5Do4/miZ8FhEwd5P8=
github.com/blastrain/vitess-sqlparser v0.0.0-20201030050434-a139afbb1aba/go.mod h1:FGQp+RNQwVmLzDq6HBrYCww9qJQyNwH9Qji/quTQII4=
github.com/bojand/ghz v0.110.0 h1:P7G26B573UeC+XvRevse5M0tP8cmkG0EQm/kS/eHUjM=
github.com/bojand/ghz v0.110.0/go.mod h1:pej2JQkTDjMckjsPsIwSVjuup2ZWr0hD/4vUrtGQm18=
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/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c h1:HIGF0r/56+7fuIZw2V4isE22MK6xpxWx7BbV8dJ290w=
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -144,6 +140,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
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.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -185,10 +182,8 @@ github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
@ -199,6 +194,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-kratos/aegis v0.1.3 h1:hXw/iO51ofO0hSRBijbmGNidthfAo9Oc/PZSl/lRCxk=
github.com/go-kratos/aegis v0.1.3/go.mod h1:jYeSQ3Gesba478zEnujOiG5QdsyF3Xk/8owFUeKcHxw=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@ -208,6 +205,8 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -301,7 +300,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -334,7 +332,6 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
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 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
@ -342,8 +339,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2
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/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/gunerhuseyin/goprometheus v0.0.2 h1:CQNUdyoGmBPDBX3mTIEUo+jcl+uwe/ISxxaSbJlGsP8=
github.com/gunerhuseyin/goprometheus v0.0.2/go.mod h1:Nd+CBNGDNC3Qrx7NMuS3cAEfI99HZp7kcqFiMfK1TAg=
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=
@ -395,6 +390,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w=
github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg=
github.com/jinzhu/configor v1.1.1 h1:gntDP+ffGhs7aJ0u8JvjCDts2OsxsI7bnz3q+jC+hSY=
@ -421,8 +418,13 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68 h1:d2hBkTvi7B89+OXY8+bBBshPlc+7JYacGrG/dFak8SQ=
github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b h1:Rrp0ByJXEjhREMPGTt3aWYjoIsUGCbt21ekbeJcTWv0=
github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -457,7 +459,6 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
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.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
@ -566,32 +567,26 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
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/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
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/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad h1:WtSUHi5zthjudjIi3L6QmL/V9vpJPbc/j/F2u55d3fs=
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad/go.mod h1:h0+DiDRe2Y+6iHTjIq/9HzUq7NII/Nffp0HkFrsAKq4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/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.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
@ -600,6 +595,8 @@ github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfA
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/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -628,8 +625,11 @@ github.com/swaggo/gin-swagger v1.5.2 h1:dj2es17EaOHoy0Owu4xn3An1mI8/xjdFyIH6KAbO
github.com/swaggo/gin-swagger v1.5.2/go.mod h1:Cbj/MlHApPOjZdf4joWFXLLgmZVPyh54GPvPPyVjVZM=
github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo=
github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@ -667,14 +667,19 @@ go.opentelemetry.io/contrib v1.9.0 h1:2KAoCVu4OMI9TYoSWvcV7+UbbIPOi4623S77nV+M/K
go.opentelemetry.io/contrib v1.9.0/go.mod h1:yp0N4+hnpWCpnMzs6T6WbD9Amfg7reEZsS0jAd/5M2Q=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0 h1:PNEMW4EvpNQ7SuoPFNkvbZqi1STkTPKq+8vfoMl/6AE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0/go.mod h1:fk1+icoN47ytLSgkoWHLJrtVTSQ+HgmkNgPTKrk/Nsc=
go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY=
go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
go.opentelemetry.io/otel v0.17.0/go.mod h1:Oqtdxmf7UtEvL037ohlgnaYa1h7GtMh0NcSd9eqkC9s=
go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw=
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
go.opentelemetry.io/otel/exporters/jaeger v1.9.0 h1:gAEgEVGDWwFjcis9jJTOJqZNxDzoZfR12WNIxr7g9Ww=
go.opentelemetry.io/otel/exporters/jaeger v1.9.0/go.mod h1:hquezOLVAybNW6vanIxkdLXTXvzlj2Vn3wevSP15RYs=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.9.0 h1:0uV0qzHk48i1SF8qRI8odMYiwPOLh9gBhiJFpj8H6JY=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.9.0/go.mod h1:Fl1iS5ZhWgXXXTdJMuBSVsS5nkL5XluHbg97kjOuYU4=
go.opentelemetry.io/otel/metric v0.17.0/go.mod h1:hUz9lH1rNXyEwWAhIWCMFWKhYtpASgSnObJFnU26dJ0=
go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs=
go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A=
go.opentelemetry.io/otel/oteltest v0.17.0/go.mod h1:JT/LGFxPwpN+nlsTiinSYjdIx3hZIGqHCpChcIZmdoE=
go.opentelemetry.io/otel/sdk v1.9.0 h1:LNXp1vrr83fNXTHgU8eO89mhzxb/bbWAsHG6fNf3qWo=
go.opentelemetry.io/otel/sdk v1.9.0/go.mod h1:AEZc8nt5bd2F7BC24J5R0mrjYnpEgYHyTcM/vrSple4=
go.opentelemetry.io/otel/trace v0.17.0/go.mod h1:bIujpqg6ZL6xUTubIUgziI1jSaUPthmabA/ygf/6Cfg=
@ -853,6 +858,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/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=
@ -909,6 +915,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -927,6 +934,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20180302201248-b7ef84aaf62a/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -1187,11 +1195,12 @@ 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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=

View File

@ -9,9 +9,9 @@ import (
var config *Config
func Init(configFile string) error {
func Init(configFile string, fs ...func()) error {
config = &Config{}
return conf.Parse(configFile, config)
return conf.Parse(configFile, config, fs...)
}
func Show() string {
@ -30,15 +30,14 @@ func Set(conf *Config) {
}
type Config struct {
App App `yaml:"app" json:"app"`
Etcd Etcd `yaml:"etcd" json:"etcd"`
Grpc Grpc `yaml:"grpc" json:"grpc"`
HTTP HTTP `yaml:"http" json:"http"`
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
Logger Logger `yaml:"logger" json:"logger"`
Mysql Mysql `yaml:"mysql" json:"mysql"`
RateLimiter RateLimiter `yaml:"rateLimiter" json:"rateLimiter"`
Redis Redis `yaml:"redis" json:"redis"`
App App `yaml:"app" json:"app"`
Etcd Etcd `yaml:"etcd" json:"etcd"`
Grpc Grpc `yaml:"grpc" json:"grpc"`
HTTP HTTP `yaml:"http" json:"http"`
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
Logger Logger `yaml:"logger" json:"logger"`
Mysql Mysql `yaml:"mysql" json:"mysql"`
Redis Redis `yaml:"redis" json:"redis"`
}
type Etcd struct {
@ -67,13 +66,8 @@ type Redis struct {
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
}
type RateLimiter struct {
Dimension string `yaml:"dimension" json:"dimension"`
MaxLimit int `yaml:"maxLimit" json:"maxLimit"`
QPSLimit int `yaml:"qpsLimit" json:"qpsLimit"`
}
type App struct {
EnableCircuitBreaker bool `yaml:"enableCircuitBreaker" json:"enableCircuitBreaker"`
EnableLimit bool `yaml:"enableLimit" json:"enableLimit"`
EnableMetrics bool `yaml:"enableMetrics" json:"enableMetrics"`
EnableProfile bool `yaml:"enableProfile" json:"enableProfile"`
@ -85,19 +79,10 @@ type App struct {
Version string `yaml:"version" json:"version"`
}
type LogFileConfig struct {
Filename string `yaml:"filename" json:"filename"`
IsCompression bool `yaml:"isCompression" json:"isCompression"`
MaxAge int `yaml:"maxAge" json:"maxAge"`
MaxBackups int `yaml:"maxBackups" json:"maxBackups"`
MaxSize int `yaml:"maxSize" json:"maxSize"`
}
type Logger struct {
Format string `yaml:"format" json:"format"`
IsSave bool `yaml:"isSave" json:"isSave"`
Level string `yaml:"level" json:"level"`
LogFileConfig LogFileConfig `yaml:"logFileConfig" json:"logFileConfig"`
Format string `yaml:"format" json:"format"`
IsSave bool `yaml:"isSave" json:"isSave"`
Level string `yaml:"level" json:"level"`
}
type Grpc struct {

View File

@ -116,7 +116,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExa
err = d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
if err != nil {
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
if err.Error() == model.ErrRecordNotFound.Error() {
if errors.Is(err, model.ErrRecordNotFound) {
err = d.cache.SetCacheWithNotFound(ctx, id)
if err != nil {
return nil, err
@ -214,7 +214,7 @@ func (d *userExampleDao) GetByIDs(ctx context.Context, ids []uint64) ([]*model.U
func (d *userExampleDao) GetByColumns(ctx context.Context, params *query.Params) ([]*model.UserExample, int64, error) {
queryStr, args, err := params.ConvertToGormConditions()
if err != nil {
return nil, 0, err
return nil, 0, errors.New("query params error: " + err.Error())
}
var total int64

View File

@ -20,9 +20,10 @@ var (
StatusForbidden = errcode.StatusForbidden
StatusLimitExceed = errcode.StatusLimitExceed
StatusDeadlineExceeded = errcode.StatusDeadlineExceeded
StatusAccessDenied = errcode.StatusAccessDenied
StatusMethodNotAllowed = errcode.StatusMethodNotAllowed
StatusDeadlineExceeded = errcode.StatusDeadlineExceeded
StatusAccessDenied = errcode.StatusAccessDenied
StatusMethodNotAllowed = errcode.StatusMethodNotAllowed
StatusServiceUnavailable = errcode.StatusServiceUnavailable
)
// Any kev-value

View File

@ -19,7 +19,8 @@ var (
Forbidden = errcode.Forbidden
LimitExceed = errcode.LimitExceed
DeadlineExceeded = errcode.DeadlineExceeded
AccessDenied = errcode.AccessDenied
MethodNotAllowed = errcode.MethodNotAllowed
DeadlineExceeded = errcode.DeadlineExceeded
AccessDenied = errcode.AccessDenied
MethodNotAllowed = errcode.MethodNotAllowed
MethodServiceUnavailable = errcode.MethodServiceUnavailable
)

View File

@ -1,6 +1,8 @@
package handler
import (
"errors"
"github.com/zhufuyi/sponge/internal/cache"
"github.com/zhufuyi/sponge/internal/dao"
"github.com/zhufuyi/sponge/internal/ecode"
@ -64,14 +66,14 @@ func (h *userExampleHandler) Create(c *gin.Context) {
err = copier.Copy(userExample, form)
if err != nil {
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
response.Error(c, ecode.ErrCreateUserExample)
return
}
err = h.iDao.Create(c.Request.Context(), userExample)
if err != nil {
logger.Error("Create error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrCreateUserExample)
response.Output(c, ecode.InternalServerError.ToHTTPCode())
return
}
@ -96,7 +98,7 @@ func (h *userExampleHandler) DeleteByID(c *gin.Context) {
err := h.iDao.DeleteByID(c.Request.Context(), id)
if err != nil {
logger.Error("DeleteByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrDeleteUserExample)
response.Output(c, ecode.InternalServerError.ToHTTPCode())
return
}
@ -132,14 +134,14 @@ func (h *userExampleHandler) UpdateByID(c *gin.Context) {
err = copier.Copy(userExample, form)
if err != nil {
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
response.Error(c, ecode.ErrUpdateUserExample)
return
}
err = h.iDao.UpdateByID(c.Request.Context(), userExample)
if err != nil {
logger.Error("UpdateByID error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrUpdateUserExample)
response.Output(c, ecode.InternalServerError.ToHTTPCode())
return
}
@ -163,12 +165,12 @@ func (h *userExampleHandler) GetByID(c *gin.Context) {
userExample, err := h.iDao.GetByID(c.Request.Context(), id)
if err != nil {
if err.Error() == query.ErrNotFound.Error() {
if errors.Is(err, query.ErrNotFound) {
logger.Warn("GetByID not found", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.NotFound)
} else {
logger.Error("GetByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrGetUserExample)
response.Output(c, ecode.InternalServerError.ToHTTPCode())
}
return
}
@ -177,7 +179,7 @@ func (h *userExampleHandler) GetByID(c *gin.Context) {
err = copier.Copy(data, userExample)
if err != nil {
logger.Warn("Copy error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
response.Error(c, ecode.ErrGetUserExample)
return
}
data.ID = idStr
@ -206,14 +208,15 @@ func (h *userExampleHandler) ListByIDs(c *gin.Context) {
userExamples, err := h.iDao.GetByIDs(c.Request.Context(), form.IDs)
if err != nil {
logger.Error("GetByIDs error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrListUserExample)
response.Output(c, ecode.InternalServerError.ToHTTPCode())
return
}
data, err := convertUserExamples(userExamples)
if err != nil {
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
response.Error(c, ecode.ErrListUserExample)
return
}
@ -243,14 +246,14 @@ func (h *userExampleHandler) List(c *gin.Context) {
userExamples, total, err := h.iDao.GetByColumns(c.Request.Context(), &form.Params)
if err != nil {
logger.Error("GetByColumns error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrListUserExample)
response.Output(c, ecode.InternalServerError.ToHTTPCode())
return
}
data, err := convertUserExamples(userExamples)
if err != nil {
logger.Warn("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
response.Error(c, ecode.ErrListUserExample)
return
}

View File

@ -125,7 +125,7 @@ func Test_userExampleHandler_Create(t *testing.T) {
h.MockDao.SqlMock.ExpectCommit()
result = &gohttp.StdResult{}
err = gohttp.Post(result, h.GetRequestURL("Create"), testData)
assert.NoError(t, err)
assert.Error(t, err)
// delete the templates code end
}
@ -155,7 +155,7 @@ func Test_userExampleHandler_DeleteByID(t *testing.T) {
// delete error test
err = gohttp.Delete(result, h.GetRequestURL("DeleteByID", 111))
assert.NoError(t, err)
assert.Error(t, err)
}
func Test_userExampleHandler_UpdateByID(t *testing.T) {
@ -185,7 +185,7 @@ func Test_userExampleHandler_UpdateByID(t *testing.T) {
// update error test
err = gohttp.Put(result, h.GetRequestURL("UpdateByID", 111), testData)
assert.NoError(t, err)
assert.Error(t, err)
}
func Test_userExampleHandler_GetByID(t *testing.T) {
@ -215,7 +215,7 @@ func Test_userExampleHandler_GetByID(t *testing.T) {
// get error test
err = gohttp.Get(result, h.GetRequestURL("GetByID", 111))
assert.NoError(t, err)
assert.Error(t, err)
}
func Test_userExampleHandler_ListByIDs(t *testing.T) {
@ -243,7 +243,7 @@ func Test_userExampleHandler_ListByIDs(t *testing.T) {
// get error test
err = gohttp.Post(result, h.GetRequestURL("ListByIDs"), &types.GetUserExamplesByIDsRequest{IDs: []uint64{111}})
assert.NoError(t, err)
assert.Error(t, err)
}
func Test_userExampleHandler_List(t *testing.T) {

View File

@ -2,18 +2,16 @@ package routers
import (
"net/http"
"strings"
"github.com/zhufuyi/sponge/docs"
"github.com/zhufuyi/sponge/internal/config"
"github.com/zhufuyi/sponge/pkg/gin/handlerfunc"
"github.com/zhufuyi/sponge/pkg/gin/middleware"
"github.com/zhufuyi/sponge/pkg/gin/middleware/metrics"
"github.com/zhufuyi/sponge/pkg/gin/middleware/ratelimiter"
"github.com/zhufuyi/sponge/pkg/gin/validator"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/zhufuyi/sponge/docs"
"github.com/zhufuyi/sponge/internal/config"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
@ -53,14 +51,12 @@ func NewRouter() *gin.Engine {
// limit 中间件
if config.Get().App.EnableLimit {
opts := []ratelimiter.Option{
ratelimiter.WithQPS(config.Get().RateLimiter.QPSLimit),
ratelimiter.WithBurst(config.Get().RateLimiter.MaxLimit),
}
if strings.ToUpper(config.Get().RateLimiter.Dimension) == "IP" {
opts = append(opts, ratelimiter.WithIP())
}
r.Use(ratelimiter.QPS(opts...))
r.Use(middleware.RateLimit())
}
// circuit breaker 中间件
if config.Get().App.EnableCircuitBreaker {
r.Use(middleware.CircuitBreaker())
}
// trace 中间件

View File

@ -115,9 +115,12 @@ func (s *grpcServer) serverOptions() []grpc.ServerOption {
// limit 拦截器
if config.Get().App.EnableLimit {
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerRateLimit(
interceptor.WithRateLimitQPS(config.Get().RateLimiter.QPSLimit),
))
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerRateLimit())
}
// circuit breaker 拦截器
if config.Get().App.EnableCircuitBreaker {
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerCircuitBreaker())
}
// trace 拦截器

View File

@ -2,6 +2,8 @@ package service
import (
"context"
"errors"
"strings"
pb "github.com/zhufuyi/sponge/api/serverNameExample/v1"
"github.com/zhufuyi/sponge/internal/cache"
@ -53,13 +55,13 @@ func (s *userExampleService) Create(ctx context.Context, req *pb.CreateUserExamp
err = copier.Copy(userExample, req)
if err != nil {
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInternalServerError.Err()
return nil, ecode.StatusCreateUserExample.Err()
}
err = s.iDao.Create(ctx, userExample)
if err != nil {
logger.Error("s.iDao.Create error", logger.Err(err), logger.Any("userExample", userExample))
return nil, ecode.StatusCreateUserExample.Err()
return nil, ecode.StatusInternalServerError.ToRPCErr()
}
return &pb.CreateUserExampleReply{Id: userExample.ID}, nil
@ -76,7 +78,7 @@ func (s *userExampleService) DeleteByID(ctx context.Context, req *pb.DeleteUserE
err = s.iDao.DeleteByID(ctx, req.Id)
if err != nil {
logger.Error("s.iDao.DeleteByID error", logger.Err(err), logger.Any("id", req.Id))
return nil, ecode.StatusDeleteUserExample.Err()
return nil, ecode.StatusInternalServerError.ToRPCErr()
}
return &pb.DeleteUserExampleByIDReply{}, nil
@ -94,14 +96,14 @@ func (s *userExampleService) UpdateByID(ctx context.Context, req *pb.UpdateUserE
err = copier.Copy(userExample, req)
if err != nil {
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInternalServerError.Err()
return nil, ecode.StatusUpdateUserExample.Err()
}
userExample.ID = req.Id
err = s.iDao.UpdateByID(ctx, userExample)
if err != nil {
logger.Error("s.iDao.UpdateByID error", logger.Err(err), logger.Any("userExample", userExample))
return nil, ecode.StatusUpdateUserExample.Err()
return nil, ecode.StatusInternalServerError.ToRPCErr()
}
return &pb.UpdateUserExampleByIDReply{}, nil
@ -117,18 +119,18 @@ func (s *userExampleService) GetByID(ctx context.Context, req *pb.GetUserExample
record, err := s.iDao.GetByID(ctx, req.Id)
if err != nil {
if err.Error() == query.ErrNotFound.Error() {
if errors.Is(err, query.ErrNotFound) {
logger.Warn("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
return nil, ecode.StatusNotFound.Err()
}
logger.Error("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
return nil, ecode.StatusGetUserExample.Err()
return nil, ecode.StatusInternalServerError.ToRPCErr()
}
data, err := covertUserExample(record)
if err != nil {
logger.Warn("covertUserExample error", logger.Err(err), logger.Any("record", record))
return nil, ecode.StatusInternalServerError.Err()
return nil, ecode.StatusGetUserExample.Err()
}
return &pb.GetUserExampleByIDReply{UserExample: data}, nil
@ -145,7 +147,7 @@ func (s *userExampleService) ListByIDs(ctx context.Context, req *pb.ListUserExam
records, err := s.iDao.GetByIDs(ctx, req.Ids)
if err != nil {
logger.Error("s.iDao.GetByID error", logger.Err(err), logger.Any("ids", req.Ids))
return nil, ecode.StatusGetUserExample.Err()
return nil, ecode.StatusInternalServerError.ToRPCErr()
}
datas := []*pb.UserExample{}
@ -173,14 +175,18 @@ func (s *userExampleService) List(ctx context.Context, req *pb.ListUserExampleRe
err = copier.Copy(params, req.Params)
if err != nil {
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("params", req.Params))
return nil, ecode.StatusInternalServerError.Err()
return nil, ecode.StatusListUserExample.Err()
}
params.Size = int(req.Params.Limit)
records, total, err := s.iDao.GetByColumns(ctx, params)
if err != nil {
if strings.Contains(err.Error(), "query params error:") {
logger.Warn("s.iDao.GetByColumns error", logger.Err(err), logger.Any("params", params))
return nil, ecode.StatusInvalidParams.Err()
}
logger.Error("s.iDao.GetByColumns error", logger.Err(err), logger.Any("params", params))
return nil, ecode.StatusListUserExample.Err()
return nil, ecode.StatusInternalServerError.ToRPCErr()
}
datas := []*pb.UserExample{}

View File

@ -29,7 +29,7 @@ func initUserExampleServiceClient() pb.UserExampleServiceClient {
//grpccli.WithEnableLog(logger.Get()),
//grpccli.WithDiscovery(discovery),
//grpccli.WithEnableTrace(),
//grpccli.WithEnableHystrix("user"),
//grpccli.WithEnableCircuitBreaker(),
//grpccli.WithEnableLoadBalance(),
//grpccli.WithEnableRetry(),
//grpccli.WithEnableMetrics(),
@ -177,7 +177,7 @@ func Test_userExampleService_benchmark(t *testing.T) {
fn: func() error {
// todo test after filling in parameters
message := &pb.GetUserExampleByIDRequest{
Id: 3,
Id: 1,
}
b, err := benchmark.New(host, protoFile, "GetByID", message, 1000, importPaths...)
if err != nil {

2
pkg/cache/README.md vendored
View File

@ -41,7 +41,7 @@ func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExa
err := d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
if err != nil {
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
if err.Error() == model.ErrRecordNotFound.Error() {
if errors.Is(err, model.ErrRecordNotFound) {
err = d.cache.SetCacheWithNotFound(ctx, id)
if err != nil {
return nil, err

View File

@ -19,4 +19,4 @@ redis:
readTimeout: 2 # 读超时,单位(秒)
writeTimeout: 2 # 写超时,单位(秒)
#########
###############

View File

@ -0,0 +1,42 @@
package group
import "fmt"
type Counter struct {
Value int
}
func (c *Counter) Incr() {
c.Value++
}
func ExampleGroup_Get() {
group := NewGroup(func() interface{} {
fmt.Println("Only Once")
return &Counter{}
})
// Create a new Counter
group.Get("pass").(*Counter).Incr()
// Get the created Counter again.
group.Get("pass").(*Counter).Incr()
// Output:
// Only Once
}
func ExampleGroup_Reset() {
group := NewGroup(func() interface{} {
return &Counter{}
})
// Reset the new function and clear all created objects.
group.Reset(func() interface{} {
fmt.Println("reset")
return &Counter{}
})
// Create a new Counter
group.Get("pass").(*Counter).Incr()
// Output:reset
}

View File

@ -0,0 +1,64 @@
// Package group provides a sample lazy load container.
// The group only creating a new object not until the object is needed by user.
// And it will cache all the objects to reduce the creation of object.
package group
import "sync"
// Group is a lazy load container.
type Group struct {
new func() interface{}
vals map[string]interface{}
sync.RWMutex
}
// NewGroup news a group container.
func NewGroup(new func() interface{}) *Group {
if new == nil {
panic("container.group: can't assign a nil to the new function")
}
return &Group{
new: new,
vals: make(map[string]interface{}),
}
}
// Get gets the object by the given key.
func (g *Group) Get(key string) interface{} {
g.RLock()
v, ok := g.vals[key]
if ok {
g.RUnlock()
return v
}
g.RUnlock()
// slow path for group don`t have specified key value
g.Lock()
defer g.Unlock()
v, ok = g.vals[key]
if ok {
return v
}
v = g.new()
g.vals[key] = v
return v
}
// Reset resets the new function and deletes all existing objects.
func (g *Group) Reset(new func() interface{}) {
if new == nil {
panic("container.group: can't assign a nil to the new function")
}
g.Lock()
g.new = new
g.Unlock()
g.Clear()
}
// Clear deletes all objects.
func (g *Group) Clear() {
g.Lock()
g.vals = make(map[string]interface{})
g.Unlock()
}

View File

@ -0,0 +1,79 @@
package group
import (
"reflect"
"testing"
)
func TestGroupGet(t *testing.T) {
count := 0
g := NewGroup(func() interface{} {
count++
return count
})
v := g.Get("key_0")
if !reflect.DeepEqual(v.(int), 1) {
t.Errorf("expect 1, actual %v", v)
}
v = g.Get("key_1")
if !reflect.DeepEqual(v.(int), 2) {
t.Errorf("expect 2, actual %v", v)
}
v = g.Get("key_0")
if !reflect.DeepEqual(v.(int), 1) {
t.Errorf("expect 1, actual %v", v)
}
if !reflect.DeepEqual(count, 2) {
t.Errorf("expect count 2, actual %v", count)
}
}
func TestGroupReset(t *testing.T) {
g := NewGroup(func() interface{} {
return 1
})
g.Get("key")
call := false
g.Reset(func() interface{} {
call = true
return 1
})
length := 0
for range g.vals {
length++
}
if !reflect.DeepEqual(length, 0) {
t.Errorf("expect length 0, actual %v", length)
}
g.Get("key")
if !reflect.DeepEqual(call, true) {
t.Errorf("expect call true, actual %v", call)
}
}
func TestGroupClear(t *testing.T) {
g := NewGroup(func() interface{} {
return 1
})
g.Get("key")
length := 0
for range g.vals {
length++
}
if !reflect.DeepEqual(length, 1) {
t.Errorf("expect length 1, actual %v", length)
}
g.Clear()
length = 0
for range g.vals {
length++
}
if !reflect.DeepEqual(length, 0) {
t.Errorf("expect length 0, actual %v", length)
}
}

View File

@ -2,6 +2,7 @@ package errcode
import (
"fmt"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@ -58,26 +59,64 @@ func (g *GRPCStatus) Err(details ...Detail) error {
return status.Errorf(g.status.Code(), "%s details = %v", g.status.Message(), dts)
}
// ToRPCCode 转换为RPC识别的错误码避免返回Unknown状态码
func ToRPCCode(code codes.Code) codes.Code {
switch code {
// ToRPCErr converted to standard RPC error
func (g *GRPCStatus) ToRPCErr(desc ...string) error {
switch g.status.Code() {
case StatusInternalServerError.status.Code():
code = codes.Internal
return toRPCErr(codes.Internal, desc...)
case StatusInvalidParams.status.Code():
code = codes.InvalidArgument
return toRPCErr(codes.InvalidArgument, desc...)
case StatusUnauthorized.status.Code():
code = codes.Unauthenticated
return toRPCErr(codes.Unauthenticated, desc...)
case StatusNotFound.status.Code():
code = codes.NotFound
return toRPCErr(codes.NotFound, desc...)
case StatusDeadlineExceeded.status.Code():
code = codes.DeadlineExceeded
return toRPCErr(codes.DeadlineExceeded, desc...)
case StatusAccessDenied.status.Code():
code = codes.PermissionDenied
return toRPCErr(codes.PermissionDenied, desc...)
case StatusLimitExceed.status.Code():
code = codes.ResourceExhausted
return toRPCErr(codes.ResourceExhausted, desc...)
case StatusMethodNotAllowed.status.Code():
code = codes.Unimplemented
return toRPCErr(codes.Unimplemented, desc...)
case StatusServiceUnavailable.status.Code():
return toRPCErr(codes.Unavailable, desc...)
}
return code
return g.status.Err()
}
func toRPCErr(code codes.Code, descs ...string) error {
var desc string
if len(descs) > 0 {
desc = strings.Join(descs, ", ")
} else {
desc = code.String()
}
return status.New(code, desc).Err()
}
// ToRPCCode converted to standard RPC error code
func (g *GRPCStatus) ToRPCCode() codes.Code {
switch g.status.Code() {
case StatusInternalServerError.status.Code():
return codes.Internal
case StatusInvalidParams.status.Code():
return codes.InvalidArgument
case StatusUnauthorized.status.Code():
return codes.Unauthenticated
case StatusNotFound.status.Code():
return codes.NotFound
case StatusDeadlineExceeded.status.Code():
return codes.DeadlineExceeded
case StatusAccessDenied.status.Code():
return codes.PermissionDenied
case StatusLimitExceed.status.Code():
return codes.ResourceExhausted
case StatusMethodNotAllowed.status.Code():
return codes.Unimplemented
case StatusServiceUnavailable.status.Code():
return codes.Unavailable
}
return g.status.Code()
}

View File

@ -34,13 +34,23 @@ func TestToRPCCode(t *testing.T) {
StatusDeadlineExceeded,
StatusAccessDenied,
StatusMethodNotAllowed,
StatusServiceUnavailable,
}
var codes []string
for _, s := range status {
codes = append(codes, ToRPCCode(s.status.Code()).String())
codes = append(codes, s.ToRPCCode().String())
}
t.Log(codes)
var errors []error
for i, s := range status {
if i%2 == 0 {
errors = append(errors, s.ToRPCErr())
continue
}
errors = append(errors, s.ToRPCErr(s.status.Message()))
}
t.Log(errors)
}
func TestGCode(t *testing.T) {

View File

@ -10,12 +10,13 @@ var (
StatusInternalServerError = NewGRPCStatus(300003, "服务内部错误")
StatusNotFound = NewGRPCStatus(300004, "资源不存在")
StatusAlreadyExists = NewGRPCStatus(300005, "资源已存在")
StatusTimeout = NewGRPCStatus(300006, "超时")
StatusTimeout = NewGRPCStatus(300006, "访问超时")
StatusTooManyRequests = NewGRPCStatus(300007, "请求过多")
StatusForbidden = NewGRPCStatus(300008, "拒绝访问")
StatusLimitExceed = NewGRPCStatus(300009, "访问限制")
StatusDeadlineExceeded = NewGRPCStatus(300010, "已超过最后期限")
StatusAccessDenied = NewGRPCStatus(300011, "拒绝访问")
StatusMethodNotAllowed = NewGRPCStatus(300012, "不允许使用的方法")
StatusDeadlineExceeded = NewGRPCStatus(300010, "已超过最后期限")
StatusAccessDenied = NewGRPCStatus(300011, "拒绝访问")
StatusMethodNotAllowed = NewGRPCStatus(300012, "不允许使用的方法")
StatusServiceUnavailable = NewGRPCStatus(300013, "服务不可用")
)

View File

@ -60,8 +60,8 @@ func (e *Error) WithDetails(details ...string) *Error {
return &newError
}
// StatusCode 对应http错误码
func (e *Error) StatusCode() int {
// ToHTTPCode 转换为http错误码
func (e *Error) ToHTTPCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK

View File

@ -35,7 +35,7 @@ func TestNewError(t *testing.T) {
}
var httpCodes []int
for _, e := range errorsCodes {
httpCodes = append(httpCodes, e.StatusCode())
httpCodes = append(httpCodes, e.ToHTTPCode())
}
t.Log(httpCodes)

View File

@ -14,7 +14,8 @@ var (
Forbidden = NewError(100008, "拒绝访问")
LimitExceed = NewError(100009, "访问限制")
DeadlineExceeded = NewError(100010, "已超过最后期限")
AccessDenied = NewError(100011, "拒绝访问")
MethodNotAllowed = NewError(100012, "不允许使用的方法")
DeadlineExceeded = NewError(100010, "已超过最后期限")
AccessDenied = NewError(100011, "拒绝访问")
MethodNotAllowed = NewError(100012, "不允许使用的方法")
MethodServiceUnavailable = NewError(100013, "服务不可用")
)

View File

@ -42,34 +42,33 @@ gin中间件插件。
<br>
### qps限流
### 限流
#### path维度的qps限流
#### 方式一:根据硬件资源自适应限流
```go
r := gin.Default()
// e.g. (1) use default
// r.Use(RateLimit())
// e.g. (2) custom parameters
r.Use(RateLimit(
WithWindow(time.Second*10),
WithBucket(100),
WithCPUThreshold(100),
WithCPUQuota(0.5),
))
```
<br>
### 熔断器
```go
r := gin.Default()
// path, 默认qps=500, burst=1000
r.Use(ratelimiter.QPS())
// path, 自定义qps=50, burst=100
r.Use(ratelimiter.QPS(
ratelimiter.WithQPS(50),
ratelimiter.WithBurst(100),
))
r.Use(CircuitBreaker())
```
#### ip维度的qps限流
```go
// ip, 自定义qps=40, burst=80
r.Use(ratelimiter.QPS(
ratelimiter.WithIP(),
ratelimiter.WithQPS(40),
ratelimiter.WithBurst(80),
))
```
<br>
### jwt鉴权
@ -131,4 +130,4 @@ func SpanDemo(serviceName string, spanName string, ctx context.Context) {
//metrics.WithIgnoreRequestMethods(http.MethodHead), // ignore request methods
//metrics.WithIgnoreRequestPaths("/ping", "/health"), // ignore request paths
))
```
```

View File

@ -0,0 +1,72 @@
package middleware
import (
"net/http"
"github.com/zhufuyi/sponge/pkg/container/group"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/gin-gonic/gin"
"github.com/go-kratos/aegis/circuitbreaker"
"github.com/go-kratos/aegis/circuitbreaker/sre"
)
// ErrNotAllowed error not allowed.
var ErrNotAllowed = circuitbreaker.ErrNotAllowed
// CircuitBreakerOption set the circuit breaker circuitBreakerOptions.
type CircuitBreakerOption func(*circuitBreakerOptions)
type circuitBreakerOptions struct {
group *group.Group
}
func defaultCircuitBreakerOptions() *circuitBreakerOptions {
return &circuitBreakerOptions{
group: group.NewGroup(func() interface{} {
return sre.NewBreaker()
}),
}
}
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) {
o.group = g
}
}
// CircuitBreaker a circuit breaker middleware
func CircuitBreaker(opts ...CircuitBreakerOption) gin.HandlerFunc {
o := defaultCircuitBreakerOptions()
o.apply(opts...)
return func(c *gin.Context) {
breaker := o.group.Get(c.FullPath()).(circuitbreaker.CircuitBreaker)
if err := breaker.Allow(); err != nil {
// NOTE: when client reject request locally,
// continue add counter let the drop ratio higher.
breaker.MarkFailed()
response.Output(c, http.StatusServiceUnavailable, err.Error())
c.Abort()
return
}
c.Next()
code := c.Writer.Status()
// NOTE: need to check internal and service unavailable error
if code == http.StatusInternalServerError || code == http.StatusServiceUnavailable || code == http.StatusGatewayTimeout {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
}

View File

@ -0,0 +1,75 @@
package middleware
import (
"math/rand"
"net/http"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/zhufuyi/sponge/pkg/container/group"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/zhufuyi/sponge/pkg/gohttp"
"github.com/zhufuyi/sponge/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/go-kratos/aegis/circuitbreaker/sre"
)
func runCircuitBreakerHTTPServer() string {
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(CircuitBreaker(WithGroup(group.NewGroup(func() interface{} {
return sre.NewBreaker()
}))))
r.GET("/hello", func(c *gin.Context) {
if rand.Int()%2 == 0 {
response.Output(c, http.StatusInternalServerError)
} else {
response.Success(c, "hello "+c.ClientIP())
}
})
go func() {
err := r.Run(serverAddr)
if err != nil {
panic(err)
}
}()
time.Sleep(time.Millisecond * 200)
return requestAddr
}
func TestCircuitBreaker(t *testing.T) {
requestAddr := runCircuitBreakerHTTPServer()
var success, failures, countBreaker int32
for j := 0; j < 5; j++ {
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
result := &gohttp.StdResult{}
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
if strings.Contains(err.Error(), ErrNotAllowed.Error()) {
atomic.AddInt32(&countBreaker, 1)
}
atomic.AddInt32(&failures, 1)
} else {
atomic.AddInt32(&success, 1)
}
}
}()
wg.Wait()
t.Logf("%s success: %d, failures: %d, breakerOpen: %d\n",
time.Now().Format(time.RFC3339Nano), success, failures, countBreaker)
}
}

View File

@ -0,0 +1,93 @@
package middleware
import (
"net/http"
"time"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/gin-gonic/gin"
rl "github.com/go-kratos/aegis/ratelimit"
"github.com/go-kratos/aegis/ratelimit/bbr"
)
// 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
}
}
// RateLimit an adaptive rate limiter middleware
func RateLimit(opts ...RateLimitOption) gin.HandlerFunc {
o := defaultRatelimitOptions()
o.apply(opts...)
limiter := bbr.NewLimiter(
bbr.WithWindow(o.window),
bbr.WithBucket(o.bucket),
bbr.WithCPUThreshold(o.cpuThreshold),
bbr.WithCPUQuota(o.cpuQuota),
)
return func(c *gin.Context) {
done, err := limiter.Allow()
if err != nil {
response.Output(c, http.StatusTooManyRequests, err.Error())
c.Abort()
return
}
c.Next()
done(rl.DoneInfo{Err: c.Request.Context().Err()})
}
}

View File

@ -0,0 +1,77 @@
package middleware
import (
"math/rand"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/zhufuyi/sponge/pkg/gohttp"
"github.com/zhufuyi/sponge/pkg/utils"
"github.com/gin-gonic/gin"
)
func runRateLimiterHTTPServer() string {
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// e.g. (1) use default
// r.Use(RateLimit())
// e.g. (2) custom parameters
r.Use(RateLimit(
WithWindow(time.Second*10),
WithBucket(200),
WithCPUThreshold(500),
WithCPUQuota(0.5),
))
r.GET("/hello", func(c *gin.Context) {
if rand.Int()%2 == 0 {
response.Output(c, http.StatusInternalServerError)
} else {
response.Success(c, "hello "+c.ClientIP())
}
})
go func() {
err := r.Run(serverAddr)
if err != nil {
panic(err)
}
}()
time.Sleep(time.Millisecond * 200)
return requestAddr
}
func TestRateLimiter(t *testing.T) {
requestAddr := runRateLimiterHTTPServer()
var success, failures int32
for j := 0; j < 10; j++ {
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
result := &gohttp.StdResult{}
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
atomic.AddInt32(&failures, 1)
} else {
atomic.AddInt32(&success, 1)
}
}
}()
wg.Wait()
t.Logf("%s success: %d, failures: %d\n",
time.Now().Format(time.RFC3339Nano), success, failures)
}
}

View File

@ -1,28 +0,0 @@
## ratelimiter
gin `path` or `ip` limit.
<br>
### Usage
```go
r := gin.Default()
// e.g. (1) default path limit, qps=500, burst=1000
// r.Use(QPS())
// e.g. (2) path limit, qps=50, burst=100
r.Use(QPS(
WithPath(),
WithQPS(50),
WithBurst(100),
))
// e.g. (3) ip limit, qps=20, burst=40
// r.Use(QPS(
// WithIP(),
// WithQPS(20),
// WithBurst(40),
// ))
```

View File

@ -1,67 +0,0 @@
package ratelimiter
import (
"golang.org/x/time/rate"
)
var (
// default qps value
defaultQPS rate.Limit = 500
// default the maximum instantaneous request spike allowed, burst >= qps
defaultBurst = 1000
// default is path limit, fault:path limit, true:ip limit
defaultIsIP = false //nolint
)
// Option set the rate limits options.
type Option func(*options)
func defaultOptions() *options {
return &options{
qps: defaultQPS,
burst: defaultBurst,
isIP: false,
}
}
type options struct {
qps rate.Limit
burst int
isIP bool // false: path limit, true: IP limit
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithQPS set the qps value
func WithQPS(qps int) Option {
return func(o *options) {
o.qps = rate.Limit(qps)
}
}
// WithBurst set the burst value, burst >= qps
func WithBurst(burst int) Option {
return func(o *options) {
o.burst = burst
}
}
// WithPath set the path limit mode
func WithPath() Option {
return func(o *options) {
o.isIP = false
}
}
// WithIP set the path limit mode
func WithIP() Option {
return func(o *options) {
o.isIP = true
}
}

View File

@ -1,94 +0,0 @@
package ratelimiter
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
var l *Limiter
// Limiter is a controller for the request rate.
type Limiter struct {
qpsLimiter sync.Map
}
// NewLimiter instantiated limiter
func NewLimiter() *Limiter {
return &Limiter{}
}
// GetLimiter get Limiter object, can be updated or query
func GetLimiter() *Limiter {
return l
}
// SetLimiter set limiter parameters
// "limit" indicates the number of token buckets to be added at a rate = value/second (e.g. 10 means 1 token every 100 ms)
// "burst" the maximum instantaneous request spike allowed
func (l *Limiter) SetLimiter(limit rate.Limit, burst int) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.FullPath()
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(limit, burst))
if !l.allow(path) {
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
return
}
c.Next()
}
}
func (l *Limiter) allow(path string) bool {
if limiter, exist := l.qpsLimiter.Load(path); exist {
if ql, ok := limiter.(*rate.Limiter); ok && !ql.Allow() {
return false
}
}
return true
}
// UpdateQPSLimiter updates the settings for a given path's QPS limiter.
func (l *Limiter) UpdateQPSLimiter(path string, limit rate.Limit, burst int) {
if limiter, exist := l.qpsLimiter.Load(path); exist {
limiter.(*rate.Limiter).SetLimit(limit)
limiter.(*rate.Limiter).SetBurst(burst)
} else {
l.qpsLimiter.Store(path, rate.NewLimiter(limit, burst))
}
}
// GetQPSLimiterStatus returns the status of a given path's QPS limiter.
func (l *Limiter) GetQPSLimiterStatus(path string) (limit rate.Limit, burst int) {
if limiter, exist := l.qpsLimiter.Load(path); exist {
return limiter.(*rate.Limiter).Limit(), limiter.(*rate.Limiter).Burst()
}
return 0, 0
}
// QPS set limit qps parameters
func QPS(opts ...Option) gin.HandlerFunc {
o := defaultOptions()
o.apply(opts...)
l = NewLimiter()
return func(c *gin.Context) {
var path string
if !o.isIP {
path = c.FullPath()
} else {
path = c.ClientIP()
}
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(o.qps, o.burst))
if !l.allow(path) {
c.Abort()
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
return
}
c.Next()
}
}

View File

@ -1,234 +0,0 @@
package ratelimiter
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/zhufuyi/sponge/pkg/gohttp"
"github.com/zhufuyi/sponge/pkg/utils"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
func runRateLimiterHTTPServer() string {
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// e.g. (1) path limit, qps=500, burst=1000
// r.Use(QPS())
// e.g. (2) path limit, qps=50, burst=100
r.Use(QPS(
WithPath(),
WithQPS(50),
WithBurst(100),
))
// e.g. (3) ip limit, qps=20, burst=40
// r.Use(QPS(
// WithIP(),
// WithQPS(20),
// WithBurst(40),
// ))
r.GET("/ping", func(c *gin.Context) {
response.Success(c, "pong "+c.ClientIP())
})
r.GET("/hello", func(c *gin.Context) {
response.Success(c, "hello "+c.ClientIP())
})
go func() {
err := r.Run(serverAddr)
if err != nil {
panic(err)
}
}()
time.Sleep(time.Millisecond * 200)
return requestAddr
}
func TestLimiter_QPS(t *testing.T) {
requestAddr := runRateLimiterHTTPServer()
success, failure := 0, 0
start := time.Now()
for i := 0; i < 150; i++ {
result := &gohttp.StdResult{}
err := gohttp.Get(result, requestAddr+"/hello")
if err != nil {
failure++
if failure%10 == 0 {
fmt.Printf("%d %v\n", i, err)
}
} else {
success++
}
}
end := time.Now().Sub(start).Seconds()
t.Logf("time=%.3fs, success=%d, failure=%d, qps=%.1f", end, success, failure, float64(success)/end)
}
func TestRateLimiter(t *testing.T) {
requestAddr := runRateLimiterHTTPServer()
var pingSuccess, pingFailures int32
var helloSuccess, helloFailures int32
for j := 0; j < 5; j++ {
wg := &sync.WaitGroup{}
for i := 0; i < 40; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
result := &gohttp.StdResult{}
if err := gohttp.Get(result, requestAddr+"/ping"); err != nil {
atomic.AddInt32(&pingFailures, 1)
} else {
atomic.AddInt32(&pingSuccess, 1)
}
}(i)
wg.Add(1)
go func(i int) {
defer wg.Done()
result := &gohttp.StdResult{}
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
atomic.AddInt32(&helloFailures, 1)
} else {
atomic.AddInt32(&helloSuccess, 1)
}
}(i)
}
wg.Wait()
fmt.Printf("%s helloSuccess: %d, helloFailures: %d pingSuccess: %d, pingFailures: %d\n", time.Now().Format(time.RFC3339Nano), helloSuccess, helloFailures, pingSuccess, pingFailures)
//time.Sleep(time.Millisecond * 200)
}
}
func TestLimiter_GetQPSLimiterStatus(t *testing.T) {
requestAddr := runRateLimiterHTTPServer()
var pingSuccess, pingFailures int32
for j := 0; j < 5; j++ {
wg := &sync.WaitGroup{}
for i := 0; i < 40; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
result := &gohttp.StdResult{}
if err := gohttp.Get(result, requestAddr+"/ping"); err != nil {
atomic.AddInt32(&pingFailures, 1)
} else {
atomic.AddInt32(&pingSuccess, 1)
}
}(i)
}
wg.Wait()
qps, _ := GetLimiter().GetQPSLimiterStatus("/ping")
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, qps)
//time.Sleep(time.Millisecond * 200)
}
}
func TestLimiter_UpdateQPSLimiter(t *testing.T) {
requestAddr := runRateLimiterHTTPServer()
var pingSuccess, pingFailures int32
for j := 0; j < 5; j++ {
wg := &sync.WaitGroup{}
for i := 0; i < 40; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
result := &gohttp.StdResult{}
if err := gohttp.Get(result, requestAddr+"/ping"); err != nil {
atomic.AddInt32(&pingFailures, 1)
} else {
atomic.AddInt32(&pingSuccess, 1)
}
}(i)
}
wg.Wait()
limit, burst := GetLimiter().GetQPSLimiterStatus("/ping")
GetLimiter().UpdateQPSLimiter("/ping", limit+rate.Limit(j), burst)
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, limit)
//time.Sleep(time.Millisecond * 200)
}
}
func runRateLimiterHTTPServer2() string {
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
l := NewLimiter()
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(l.SetLimiter(10, 20))
r.Use(QPS(
WithIP(),
WithQPS(10),
WithBurst(20),
))
r.GET("/hello", func(c *gin.Context) {
response.Success(c, "hello "+c.ClientIP())
})
go func() {
err := r.Run(serverAddr)
if err != nil {
panic(err)
}
}()
time.Sleep(time.Millisecond * 200)
return requestAddr
}
func TestRateLimiter2(t *testing.T) {
requestAddr := runRateLimiterHTTPServer2()
var pingSuccess, pingFailures int32
var helloSuccess, helloFailures int32
for j := 0; j < 3; j++ {
wg := &sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
result := &gohttp.StdResult{}
if err := gohttp.Get(result, requestAddr+"/hello"); err != nil {
atomic.AddInt32(&helloFailures, 1)
} else {
atomic.AddInt32(&helloSuccess, 1)
}
}(i)
}
wg.Wait()
fmt.Printf("%s helloSuccess: %d, helloFailures: %d pingSuccess: %d, pingFailures: %d\n", time.Now().Format(time.RFC3339Nano), helloSuccess, helloFailures, pingSuccess, pingFailures)
//time.Sleep(time.Millisecond * 200)
}
}

View File

@ -80,6 +80,10 @@ func Output(c *gin.Context, code int, msg ...interface{}) {
respJSONWithStatusCode(c, http.StatusConflict, errcode.AlreadyExists.Msg(), msg...)
case http.StatusInternalServerError:
respJSONWithStatusCode(c, http.StatusInternalServerError, errcode.InternalServerError.Msg(), msg...)
case http.StatusTooManyRequests:
respJSONWithStatusCode(c, http.StatusTooManyRequests, errcode.LimitExceed.Msg(), msg...)
case http.StatusServiceUnavailable:
respJSONWithStatusCode(c, http.StatusServiceUnavailable, errcode.MethodServiceUnavailable.Msg(), msg...)
default:
respJSONWithStatusCode(c, code, http.StatusText(code), msg...)

48
pkg/gobash/README.md Normal file
View File

@ -0,0 +1,48 @@
## gobash
在go环境中执行命令、脚本、可执行文件日志实时输出。
<br>
## 安装
> go get -u github.com/zhufuyi/pkg/gobash
<br>
## 使用示例
### Run
Run执行命令可以主动结束命令实时返回日志和错误信息推荐使用
```go
command := "for i in $(seq 1 5); do echo 'test cmd' $i;sleep 1; done"
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) // 超时控制
// 执行
result := Run(ctx, command)
// 实时输出日志和错误信息
for v := range result.StdOut {
fmt.Printf(v)
}
if result.Err != nil {
fmt.Println("exec command failed,", result.Err.Error())
}
```
<br>
### Exec
Exec 适合执行单条非阻塞命令,输出标准和错误日志,但日志输出不是实时,注:如果执行命令永久阻塞,会造成协程泄露
```go
command := "for i in $(seq 1 5); do echo 'test cmd' $i;sleep 1; done"
out, err := gobash.Exec(command)
if err != nil {
return
}
fmt.Println(string(out))
```

135
pkg/gobash/gobash.go Normal file
View File

@ -0,0 +1,135 @@
package gobash
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os/exec"
"strings"
)
// linux default executor
var executor = "/bin/bash"
// SetExecutorPath 设置执行器
func SetExecutorPath(path string) {
executor = path
}
// Exec 适合执行单条非阻塞命令,输出标准和错误日志,但日志输出不是实时,
// 注:如果执行命令永久阻塞,会造成协程泄露
func Exec(command string) ([]byte, error) {
cmd := exec.Command(executor, "-c", command)
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
}
// Run 执行命令可以主动结束命令执行结果实时返回在Result.StdOut中
func Run(ctx context.Context, command string) *Result {
result := &Result{StdOut: make(chan string), Err: error(nil)}
go func() {
defer func() { close(result.StdOut) }() // 执行完毕,关闭通道
cmd := exec.CommandContext(ctx, executor, "-c", command)
handleExec(ctx, cmd, result)
}()
return result
}
// Result 执行命令的结果
type Result struct {
StdOut chan string
Err error // 执行完毕命令后如果为nil执行命令成功
}
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)
// 实时读取每行内容
line := ""
for {
line, err = reader.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) { // 判断是否已经读取完毕
break
}
result.Err = err
break
}
select {
case result.StdOut <- line:
case <-ctx.Done():
result.Err = fmt.Errorf("%v", ctx.Err())
return
}
}
// 捕获错误日志
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
}

52
pkg/gobash/gobash_test.go Normal file
View File

@ -0,0 +1,52 @@
package gobash
import (
"context"
"runtime"
"testing"
"time"
)
func init() {
if runtime.GOOS == "windows" {
SetExecutorPath("D:\\Program Files\\cmder\\vendor\\git-for-windows\\bin\\bash.exe")
}
}
func TestRun(t *testing.T) {
cmds := []string{
"for i in $(seq 1 3); do exit 1; done",
"notFoundCommand",
"pwd",
"for i in $(seq 1 5); do echo 'test cmd' $i;sleep 0.2; done",
}
for _, cmd := range cmds {
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*500) // 超时控制
result := Run(ctx, cmd) // 执行
for v := range result.StdOut { // 实时输出日志和错误信息
t.Logf(v)
}
if result.Err != nil {
t.Logf("exec command failed, %v", result.Err)
}
}
}
func TestExec(t *testing.T) {
cmds := []string{
"for i in $(seq 1 3); do exit 1; done",
"notFoundCommand",
"pwd",
"for i in $(seq 1 3); do echo 'test cmd' $i;sleep 0.2; done",
}
for _, cmd := range cmds {
out, err := Exec(cmd)
if err != nil {
t.Logf("exec command[%s] failed, %v\n", cmd, err)
continue
}
t.Logf("%s\n", out)
}
}

View File

@ -10,7 +10,10 @@ import (
// IsExists 判断文件或文件夹是否存在
func IsExists(path string) bool {
_, err := os.Stat(path)
return err == nil
if err != nil {
return !os.IsNotExist(err)
}
return true
}
// GetRunPath 获取程序执行的绝对路径
@ -29,10 +32,15 @@ func GetFilename(filePath string) string {
return name
}
// IsWindows 判断是否window环境
func IsWindows() bool {
return runtime.GOOS == "windows"
}
// GetPathDelimiter 根据系统类型获取分隔符
func GetPathDelimiter() string {
delimiter := "/"
if runtime.GOOS == "windows" {
if IsWindows() {
delimiter = "\\"
}

View File

@ -8,10 +8,10 @@ import (
)
func TestIsExists(t *testing.T) {
ok := IsExists("/tmp/test")
if !ok {
t.Log("not exists")
}
ok := IsExists("/tmp/tmp/tmp")
assert.Equal(t, false, ok)
ok = IsExists("README.md")
assert.Equal(t, true, ok)
}
func TestGetRunPath(t *testing.T) {
@ -78,10 +78,36 @@ func TestGetPathDelimiter(t *testing.T) {
t.Log(d)
}
func TestError(t *testing.T) {
func TestNotMatch(t *testing.T) {
fn := matchPrefix("")
assert.Equal(t, false, fn("."))
fn = matchContain("")
assert.Equal(t, false, fn("."))
fn = matchSuffix("")
assert.NotNil(t, fn)
}
func TestIsWindows(t *testing.T) {
t.Log(IsWindows())
}
func TestErrorPath(t *testing.T) {
dir := "/notfound"
_, err := ListFiles(dir)
assert.Error(t, err)
_, err = ListDirsAndFiles(dir)
assert.Error(t, err)
err = walkDirWithFilter(dir, nil, nil)
assert.Error(t, err)
err = walkDir(dir, nil)
assert.Error(t, err)
err = walkDir2(dir, nil, nil)
assert.Error(t, err)
}

View File

@ -21,8 +21,8 @@ func grpcClientExample() pb.UserExampleServiceClient {
conn, err := grpccli.DialInsecure(ctx, endpoint,
grpccli.WithEnableLog(logger.Get()),
grpccli.WithDiscovery(discovery),
//grpccli.WithEnableCircuitBreaker(),
//grpccli.WithEnableTrace(),
//grpccli.WithEnableHystrix("user"),
//grpccli.WithEnableLoadBalance(),
//grpccli.WithEnableRetry(),
//grpccli.WithEnableMetrics(),

View File

@ -66,9 +66,9 @@ func dial(ctx context.Context, endpoint string, isSecure bool, opts ...Option) (
clientOptions = append(clientOptions, grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`))
}
// 熔断器 hystrix
if o.enableHystrix {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientHystrix(o.hystrixName))
// 熔断器
if o.enableCircuitBreaker {
unaryClientInterceptors = append(unaryClientInterceptors, interceptor.UnaryClientCircuitBreaker())
}
// 重试 retry

View File

@ -29,7 +29,7 @@ func Test_dial(t *testing.T) {
WithEnableLog(zap.NewNop()),
WithEnableMetrics(),
WithEnableLoadBalance(),
WithEnableHystrix("foo"),
WithEnableCircuitBreaker(),
WithEnableRetry(),
WithDiscovery(etcd.New(&clientv3.Client{})),
)

View File

@ -25,12 +25,11 @@ type options struct {
enableLog bool // 是否开启日志
log *zap.Logger
enableTrace bool // 是否开启链路跟踪
enableMetrics bool // 是否开启指标
enableRetry bool // 是否开启重试
enableLoadBalance bool // 是否开启负载均衡器
enableHystrix bool // 是否开启熔断
hystrixName string // hystrix命令名称
enableTrace bool // 是否开启链路跟踪
enableMetrics bool // 是否开启指标
enableRetry bool // 是否开启重试
enableLoadBalance bool // 是否开启负载均衡器
enableCircuitBreaker bool // 是否开启熔断器
discovery registry.Discovery // 服务发现接口
}
@ -97,11 +96,10 @@ func WithEnableRetry() Option {
}
}
// WithEnableHystrix enable hystrix
func WithEnableHystrix(name string) Option {
// WithEnableCircuitBreaker enable circuit breaker
func WithEnableCircuitBreaker() Option {
return func(o *options) {
o.enableHystrix = true
o.hystrixName = name
o.enableCircuitBreaker = true
}
}

View File

@ -37,12 +37,11 @@ func TestWithDiscovery(t *testing.T) {
assert.NotEqual(t, testData, o.discovery)
}
func TestWithEnableHystrix(t *testing.T) {
testData := "hystrix"
opt := WithEnableHystrix(testData)
func TestWithEnableCircuitBreaker(t *testing.T) {
opt := WithEnableCircuitBreaker()
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.hystrixName)
assert.Equal(t, true, o.enableCircuitBreaker)
}
func TestWithEnableLoadBalance(t *testing.T) {

View File

@ -1,39 +0,0 @@
## hystrix
### 使用示例
#### grpc client
```go
func getDialOptions() []grpc.DialOption {
var options []grpc.DialOption
// 禁止tls加密
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
// 熔断拦截器
option := grpc.WithUnaryInterceptor(
grpc_middleware.ChainUnaryClient(
hystrix.UnaryClientInterceptor("hello_grpc",
hystrix.WithTimeout(time.Second*2), // 执行command的超时时间时间单位是ms默认时间是1000ms
hystrix.WithMaxConcurrentRequests(20), // command的最大并发量默认值是10并发量
hystrix.WithSleepWindow(10*time.Second), // 熔断器被打开后使用在熔断器被打开后根据SleepWindow设置的时间控制多久后尝试服务是否可用默认时间为5000ms
hystrix.WithRequestVolumeThreshold(1000), // 判断熔断开关的条件之一统计10s代码中写死了内请求数量达到这个请求数量后再根据错误率判断是否要开启熔断
hystrix.WithErrorPercentThreshold(25), // 判断熔断开关的条件之一统计错误百分比请求数量大于等于RequestVolumeThreshold并且错误率到达这个百分比后就会启动熔断 默认值是50
),
),
)
options = append(options, option)
return options
}
func main() {
conn, err := grpc.Dial("127.0.0.1:8080", getDialOptions()...)
if err != nil {
panic(err)
}
// ......
}
```

View File

@ -1,107 +0,0 @@
package hystrix
import (
"context"
"time"
"github.com/afex/hystrix-go/hystrix"
metricCollector "github.com/afex/hystrix-go/hystrix/metric_collector"
"github.com/afex/hystrix-go/plugins"
"google.golang.org/grpc"
)
// https://github.com/soyacen/grpc-middleware/tree/main/hystrix
// UnaryClientInterceptor set the hystrix of unary client interceptor
func UnaryClientInterceptor(commandName string, opts ...Option) grpc.UnaryClientInterceptor {
o := defaultOptions()
o.apply(opts...)
if o.statsD != nil {
c, err := plugins.InitializeStatsdCollector(o.statsD)
if err != nil {
panic(err)
}
metricCollector.Registry.Register(c.NewStatsdCollector)
}
hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{
Timeout: durationToInt(o.timeout, time.Millisecond),
MaxConcurrentRequests: o.maxConcurrentRequests,
RequestVolumeThreshold: o.requestVolumeThreshold,
SleepWindow: durationToInt(o.sleepWindow, time.Millisecond),
ErrorPercentThreshold: o.errorPercentThreshold,
})
return unaryClientInterceptor(commandName, o)
}
func unaryClientInterceptor(commandName string, o *options) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) {
err = hystrix.DoC(ctx, commandName,
// 熔断开关
func(ctx context.Context) error {
err = invoker(ctx, method, req, reply, cc, opts...)
if err != nil {
return err
}
return nil
},
// 降级处理
o.fallbackFunc,
)
return err
}
}
// StreamClientInterceptor set the hystrix of clientStream client interceptor
func StreamClientInterceptor(commandName string, opts ...Option) grpc.StreamClientInterceptor {
o := defaultOptions()
o.apply(opts...)
if o.statsD != nil {
c, err := plugins.InitializeStatsdCollector(o.statsD)
if err != nil {
panic(err)
}
metricCollector.Registry.Register(c.NewStatsdCollector)
}
hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{
Timeout: durationToInt(o.timeout, time.Millisecond),
MaxConcurrentRequests: o.maxConcurrentRequests,
RequestVolumeThreshold: o.requestVolumeThreshold,
SleepWindow: durationToInt(o.sleepWindow, time.Millisecond),
ErrorPercentThreshold: o.errorPercentThreshold,
})
return streamClientInterceptor(commandName, o)
}
func streamClientInterceptor(commandName string, o *options) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
var clientStream grpc.ClientStream
err := hystrix.DoC(ctx, commandName,
// 熔断开关
func(ctx context.Context) error {
var err error
clientStream, err = streamer(ctx, desc, cc, method, opts...)
if err != nil {
return err
}
return nil
},
// 降级处理
o.fallbackFunc,
)
return clientStream, err
}
}
func durationToInt(duration time.Duration, unit time.Duration) int {
durationAsNumber := duration / unit
if int64(durationAsNumber) > int64(maxInt) {
return maxInt
}
return int(durationAsNumber)
}

View File

@ -1,69 +0,0 @@
package hystrix
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func TestUnaryClientInterceptor(t *testing.T) {
interceptor := UnaryClientInterceptor("hystrix",
WithStatsDCollector("localhost:5555", "hystrix", 0.5, 2048))
assert.NotNil(t, interceptor)
err := interceptor(context.Background(), "test.ping", nil, nil, nil, clientInvoker)
assert.NoError(t, err)
}
func TestStreamClientInterceptor(t *testing.T) {
interceptor := StreamClientInterceptor("hystrix",
WithStatsDCollector("localhost:5555", "hystrix", 0.5, 2048))
assert.NotNil(t, interceptor)
streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return &clientStream{}, nil
}
_, err := interceptor(context.Background(), nil, nil, "test.ping", streamer)
assert.NoError(t, err)
}
func Test_durationToInt(t *testing.T) {
durationToInt(10*time.Second, time.Second)
}
// ------------------------------------------------------------------------------------------
var clientInvoker = func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
return nil
}
type clientStream struct {
}
func (s clientStream) Header() (metadata.MD, error) {
return metadata.MD{}, nil
}
func (s clientStream) Trailer() metadata.MD {
return metadata.MD{}
}
func (s clientStream) CloseSend() error {
return nil
}
func (s clientStream) Context() context.Context {
return context.Background()
}
func (s clientStream) SendMsg(m interface{}) error {
return nil
}
func (s clientStream) RecvMsg(m interface{}) error {
return nil
}

View File

@ -1,116 +0,0 @@
package hystrix
import (
"context"
"time"
"github.com/afex/hystrix-go/plugins"
"github.com/gunerhuseyin/goprometheus"
hystrixmiddleware "github.com/gunerhuseyin/goprometheus/middleware/hystrix"
)
const (
defaultHystrixTimeout = 30 * time.Second
defaultMaxConcurrentRequests = 100
defaultErrorPercentThreshold = 25
defaultSleepWindow = 10
defaultRequestVolumeThreshold = 10
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
)
func defaultOptions() *options {
return &options{
fallbackFunc: nil,
timeout: defaultHystrixTimeout,
maxConcurrentRequests: defaultMaxConcurrentRequests,
errorPercentThreshold: defaultErrorPercentThreshold,
sleepWindow: defaultSleepWindow,
requestVolumeThreshold: defaultRequestVolumeThreshold,
}
}
// options is the hystrix client implementation
type options struct {
timeout time.Duration
maxConcurrentRequests int
requestVolumeThreshold int
sleepWindow time.Duration
errorPercentThreshold int
fallbackFunc func(ctx context.Context, err error) error
statsD *plugins.StatsdCollectorConfig
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// Option represents the hystrix client options
type Option func(*options)
// WithTimeout sets hystrix timeout
func WithTimeout(timeout time.Duration) Option {
return func(c *options) {
c.timeout = timeout
}
}
// WithMaxConcurrentRequests sets hystrix max concurrent requests
func WithMaxConcurrentRequests(maxConcurrentRequests int) Option {
return func(c *options) {
c.maxConcurrentRequests = maxConcurrentRequests
}
}
// WithRequestVolumeThreshold sets hystrix request volume threshold
func WithRequestVolumeThreshold(requestVolumeThreshold int) Option {
return func(c *options) {
c.requestVolumeThreshold = requestVolumeThreshold
}
}
// WithSleepWindow sets hystrix sleep window
func WithSleepWindow(sleepWindow time.Duration) Option {
return func(c *options) {
c.sleepWindow = sleepWindow
}
}
// WithErrorPercentThreshold sets hystrix error percent threshold
func WithErrorPercentThreshold(errorPercentThreshold int) Option {
return func(c *options) {
c.errorPercentThreshold = errorPercentThreshold
}
}
// WithFallbackFunc sets the fallback function
func WithFallbackFunc(fn func(ctx context.Context, err error) error) Option {
return func(c *options) {
c.fallbackFunc = fn
}
}
// WithPrometheus sets the hystrix metrics
func WithPrometheus() Option {
return func(c *options) {
gpm := goprometheus.New()
// 采集hystrix指标
gpHystrixConfig := &hystrixmiddleware.Config{
Prefix: "hystrix_circuit_breaker_",
}
gpHystrix := hystrixmiddleware.New(gpm, gpHystrixConfig)
gpm.UseHystrix(gpHystrix)
gpm.Run() // 添加go prometheus路由/metrics
}
}
// WithStatsDCollector exports hystrix metrics to a statsD backend
func WithStatsDCollector(addr string, prefix string, sampleRate float32, flushBytes int) Option {
return func(c *options) {
c.statsD = &plugins.StatsdCollectorConfig{StatsdAddr: addr, Prefix: prefix, SampleRate: sampleRate, FlushBytes: flushBytes}
}
}

View File

@ -1,86 +0,0 @@
package hystrix
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestWithErrorPercentThreshold(t *testing.T) {
testData := 50
opt := WithErrorPercentThreshold(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.errorPercentThreshold)
}
func TestWithFallbackFunc(t *testing.T) {
testData := func(ctx context.Context, err error) error {
t.Log("this is fall back")
return nil
}
opt := WithFallbackFunc(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, nil, o.fallbackFunc(context.Background(), nil))
}
func TestWithMaxConcurrentRequests(t *testing.T) {
testData := 1000
opt := WithMaxConcurrentRequests(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.maxConcurrentRequests)
}
func TestWithPrometheus(t *testing.T) {
opt := WithPrometheus()
o := new(options)
o.apply(opt)
}
func TestWithRequestVolumeThreshold(t *testing.T) {
testData := 1000
opt := WithRequestVolumeThreshold(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.requestVolumeThreshold)
}
func TestWithSleepWindow(t *testing.T) {
testData := time.Second * 10
opt := WithSleepWindow(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.sleepWindow)
}
func TestWithStatsDCollector(t *testing.T) {
opt := WithStatsDCollector("localhost:5555", "hystrix", 0.5, 2048)
o := new(options)
o.apply(opt)
assert.Equal(t, "hystrix", o.statsD.Prefix)
}
func TestWithTimeout(t *testing.T) {
testData := time.Second * 10
opt := WithTimeout(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.timeout)
}
func Test_defaultOptions(t *testing.T) {
o := defaultOptions()
assert.NotNil(t, o)
}
func Test_options_apply(t *testing.T) {
testData := time.Second * 10
opt := WithTimeout(testData)
o := new(options)
o.apply(opt)
assert.Equal(t, testData, o.timeout)
}

View File

@ -121,6 +121,53 @@ func getDialOptions() []grpc.DialOption {
<br>
#### 限流
```go
func getDialOptions() []grpc.DialOption {
var options []grpc.DialOption
// 禁用tls
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
// circuit breaker
option := grpc.WithUnaryInterceptor(
grpc_middleware.ChainUnaryClient(
interceptor.UnaryRateLimit(),
),
)
options = append(options, option)
return options
}
```
<br>
#### 熔断器
```go
func getDialOptions() []grpc.DialOption {
var options []grpc.DialOption
// 禁用tls
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
// circuit breaker
option := grpc.WithUnaryInterceptor(
grpc_middleware.ChainUnaryClient(
interceptor.UnaryClientCircuitBreaker(),
),
)
options = append(options, option)
return options
}
```
<br>
#### timeout
```go
@ -209,8 +256,3 @@ func SpanDemo(serviceName string, spanName string, ctx context.Context) {
使用示例 [metrics](../metrics/README.md)。
<br>
#### hystrix
使用示例 [hystrix](../hystrix/README.md)。

View File

@ -0,0 +1,161 @@
package interceptor
import (
"context"
"github.com/zhufuyi/sponge/pkg/container/group"
"github.com/zhufuyi/sponge/pkg/errcode"
"github.com/go-kratos/aegis/circuitbreaker"
"github.com/go-kratos/aegis/circuitbreaker/sre"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ErrNotAllowed error not allowed.
var ErrNotAllowed = circuitbreaker.ErrNotAllowed
// CircuitBreakerOption set the circuit breaker circuitBreakerOptions.
type CircuitBreakerOption func(*circuitBreakerOptions)
type circuitBreakerOptions struct {
group *group.Group
}
func defaultCircuitBreakerOptions() *circuitBreakerOptions {
return &circuitBreakerOptions{
group: group.NewGroup(func() interface{} {
return sre.NewBreaker()
}),
}
}
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) {
o.group = g
}
}
// 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,
// continue add 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)
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
return err
}
}
// SteamClientCircuitBreaker client-side stream circuit breaker interceptor
func SteamClientCircuitBreaker(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,
// continue add 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)
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
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) (resp interface{}, err error) {
breaker := o.group.Get(info.FullMethod).(circuitbreaker.CircuitBreaker)
if err := breaker.Allow(); err != nil {
// NOTE: when client reject request locally,
// continue add counter let the drop ratio higher.
breaker.MarkFailed()
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)
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
return reply, err
}
}
// SteamServerCircuitBreaker server-side stream circuit breaker interceptor
func SteamServerCircuitBreaker(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,
// continue add 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)
if ok && (s.Code() == codes.Internal || s.Code() == codes.Unavailable) {
breaker.MarkFailed()
} else {
breaker.MarkSuccess()
}
}
return err
}
}

View File

@ -0,0 +1,93 @@
package interceptor
import (
"context"
"testing"
"github.com/zhufuyi/sponge/pkg/container/group"
"github.com/zhufuyi/sponge/pkg/errcode"
"github.com/go-kratos/aegis/circuitbreaker/sre"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
)
func TestUnaryClientCircuitBreaker(t *testing.T) {
interceptor := UnaryClientCircuitBreaker(WithGroup(
group.NewGroup(func() interface{} {
return sre.NewBreaker()
}),
))
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 := SteamClientCircuitBreaker()
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) {
interceptor := UnaryServerCircuitBreaker()
assert.NotNil(t, interceptor)
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, errcode.StatusInternalServerError.ToRPCErr()
}
for i := 0; i < 110; i++ {
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/test"}, handler)
assert.Error(t, err)
}
handler = func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, errcode.StatusInvalidParams.Err()
}
_, err := interceptor(context.Background(), nil, &grpc.UnaryServerInfo{FullMethod: "/test"}, handler)
assert.Error(t, err)
}
func TestSteamServerCircuitBreaker(t *testing.T) {
interceptor := SteamServerCircuitBreaker()
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)
}

View File

@ -1,17 +0,0 @@
package interceptor
import (
"github.com/zhufuyi/sponge/pkg/grpc/hystrix"
"google.golang.org/grpc"
)
// UnaryClientHystrix 客户端熔断器unary拦截器
func UnaryClientHystrix(commandName string, opts ...hystrix.Option) grpc.UnaryClientInterceptor {
return hystrix.UnaryClientInterceptor(commandName, opts...)
}
// SteamClientHystrix 客户端熔断器stream拦截器
func SteamClientHystrix(commandName string, opts ...hystrix.Option) grpc.StreamClientInterceptor {
return hystrix.StreamClientInterceptor(commandName, opts...)
}

View File

@ -1,17 +0,0 @@
package interceptor
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnaryClientHystrix(t *testing.T) {
interceptor := UnaryClientHystrix("demo")
assert.NotNil(t, interceptor)
}
func TestSteamClientHystrix(t *testing.T) {
interceptor := SteamClientHystrix("demo")
assert.NotNil(t, interceptor)
}

View File

@ -1,89 +1,116 @@
package interceptor
import (
"context"
"time"
"github.com/grpc-ecosystem/go-grpc-middleware/ratelimit"
"github.com/reugn/equalizer"
"github.com/zhufuyi/sponge/pkg/errcode"
rl "github.com/go-kratos/aegis/ratelimit"
"github.com/go-kratos/aegis/ratelimit/bbr"
"google.golang.org/grpc"
)
// ---------------------------------- server interceptor ----------------------------------
// RateLimitOption 日志设置
type RateLimitOption func(*rateLimitOptions)
// ErrLimitExceed is returned when the rate limiter is
// triggered and the request is rejected due to limit exceeded.
var ErrLimitExceed = rl.ErrLimitExceed
type rateLimitOptions struct {
qps int // 允许请求速度
capacity int // 重新填充容量
refillInterval time.Duration // 填充token速度refillInterval=time.Second/qps*capacity
// 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{
qps: 1000,
capacity: 50,
refillInterval: time.Second / 1000 * 50,
func defaultRatelimitOptions() *ratelimitOptions {
return &ratelimitOptions{
window: time.Second * 10,
bucket: 100,
cpuThreshold: 800,
}
}
func (o *rateLimitOptions) apply(opts ...RateLimitOption) {
func (o *ratelimitOptions) apply(opts ...RatelimitOption) {
for _, opt := range opts {
opt(o)
}
}
// WithRateLimitQPS 设置请求qps
func WithRateLimitQPS(qps int) RateLimitOption {
return func(o *rateLimitOptions) {
o.qps = qps
if qps < 10 {
o.capacity = qps
} else if qps < 100 {
o.capacity = 10
} else if qps < 500 {
o.capacity = 40
} else if qps < 1000 {
o.capacity = 80
} else if qps < 2000 {
o.capacity = 100
} else if qps < 4000 {
o.capacity = 200
} else if qps < 10000 {
o.capacity = 400
} else {
o.capacity = 500
// 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 := bbr.NewLimiter(
bbr.WithWindow(o.window),
bbr.WithBucket(o.bucket),
bbr.WithCPUThreshold(o.cpuThreshold),
bbr.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())
}
o.refillInterval = time.Second / time.Duration(o.qps) * time.Duration(o.capacity)
reply, err := handler(ctx, req)
done(rl.DoneInfo{Err: err})
return reply, err
}
}
type myLimiter struct {
TB *equalizer.TokenBucket // 令牌桶
}
// StreamServerRateLimit server-side stream circuit breaker interceptor
func StreamServerRateLimit(opts ...RatelimitOption) grpc.StreamServerInterceptor {
o := defaultRatelimitOptions()
o.apply(opts...)
limiter := bbr.NewLimiter(
bbr.WithWindow(o.window),
bbr.WithBucket(o.bucket),
bbr.WithCPUThreshold(o.cpuThreshold),
bbr.WithCPUQuota(o.cpuQuota),
)
func (m *myLimiter) Limit() bool {
if m.TB.Ask() {
return false
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
}
return true
}
// UnaryServerRateLimit 限流unary拦截器
func UnaryServerRateLimit(opts ...RateLimitOption) grpc.UnaryServerInterceptor {
o := defaultRateLimitOptions()
o.apply(opts...)
limiter := &myLimiter{TB: equalizer.NewTokenBucket(int32(o.capacity), o.refillInterval)}
return ratelimit.UnaryServerInterceptor(limiter)
}
// StreamServerRateLimit 限流stream拦截器
func StreamServerRateLimit(opts ...RateLimitOption) grpc.StreamServerInterceptor {
o := defaultRateLimitOptions()
o.apply(opts...)
limiter := &myLimiter{equalizer.NewTokenBucket(int32(o.capacity), o.refillInterval)}
return ratelimit.StreamServerInterceptor(limiter)
}

View File

@ -1,55 +1,36 @@
package interceptor
import (
"context"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"testing"
"time"
"github.com/reugn/equalizer"
"github.com/stretchr/testify/assert"
)
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)
}
func TestUnaryServerRateLimit(t *testing.T) {
interceptor := UnaryServerRateLimit()
assert.NotNil(t, interceptor)
}
func TestWithRateLimitQPS(t *testing.T) {
testData := 1000
opt := WithRateLimitQPS(testData)
o := new(rateLimitOptions)
o.apply(opt)
assert.Less(t, time.Duration(testData), o.refillInterval)
_ = WithRateLimitQPS(5)
_ = WithRateLimitQPS(55)
_ = WithRateLimitQPS(255)
_ = WithRateLimitQPS(555)
_ = WithRateLimitQPS(1555)
_ = WithRateLimitQPS(2555)
_ = WithRateLimitQPS(5555)
_ = WithRateLimitQPS(55555)
}
func Test_defaultRateLimitOptions(t *testing.T) {
o := defaultRateLimitOptions()
assert.NotNil(t, o)
}
func Test_rateLimitOptions_apply(t *testing.T) {
testData := 1000
opt := WithRateLimitQPS(testData)
o := new(rateLimitOptions)
o.apply(opt)
assert.Less(t, time.Duration(testData), o.refillInterval)
}
func Test_myLimiter_Limit(t *testing.T) {
l := &myLimiter{equalizer.NewTokenBucket(100, 50)}
actual := l.Limit()
assert.Equal(t, false, actual)
handler := func(srv interface{}, stream grpc.ServerStream) error {
return nil
}
err := interceptor(nil, nil, nil, handler)
assert.NoError(t, err)
}

View File

@ -12,7 +12,7 @@ import (
var (
// 默认触发重试的错误码
defaultErrCodes = []codes.Code{codes.Unavailable}
defaultErrCodes = []codes.Code{codes.Internal}
)
// RetryOption set the retry retryOptions.

46
pkg/jy2struct/README.md Normal file
View File

@ -0,0 +1,46 @@
## jy2struct
json和yaml转go struct代码。
<br>
### 安装
> go get -u github.com/zhufuyi/pkg/jy2struct
<br>
### 使用示例
主要设置参数:
```go
type Args struct {
Format string // 文档格式json或yaml
Data string // json或yaml内容
InputFile string // 文件
Name string // 结构体名称
SubStruct bool // 子结构体是否分开
Tags string // 添加额外tag多个tag用逗号分隔
}
```
<br>
转换示例:
```go
// json转struct
code, err := jy2struct.Covert(&jy2struct.Args{
Format: "json",
// InputFile: "user.json", // 来源于json文件
SubStruct: true,
})
// json转struct
code, err := jy2struct.Covert(&jy2struct.Args{
Format: "yaml",
// InputFile: "user.yaml", // 来源于yaml文件
SubStruct: true,
})
```

77
pkg/jy2struct/covert.go Normal file
View File

@ -0,0 +1,77 @@
package jy2struct
import (
"bytes"
"errors"
"os"
"strings"
)
// Args 参数
type Args struct {
Format string // 文档格式json或yaml
Data string // json或yaml内容
InputFile string // 文件
Name string // 结构体名称
SubStruct bool // 子结构体是否分开
Tags string // 字段tag多个tag用逗号分隔
tags []string
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
}
// Covert json或yaml转go struct
func Covert(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
}

View File

@ -0,0 +1,80 @@
package jy2struct
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestCovert(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 := Covert(tt.args.args)
if (err != nil) != tt.wantErr {
t.Errorf("Covert() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Log(got)
})
}
// test Covert error
arg := &Args{Format: "unknown"}
_, err := Covert(arg)
assert.Error(t, err)
arg = &Args{Format: "yaml", InputFile: "notfound.yaml"}
_, err = Covert(arg)
assert.Error(t, err)
}

493
pkg/jy2struct/jy2struct.go Normal file
View File

@ -0,0 +1,493 @@
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) {
var subStructMap map[string]string = nil
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("package %s\n\ntype %s %s\n", pkgName, structName, typeForValue(iresult, structName, tags, subStructMap, convertFloats))
src := fmt.Sprintf("\ntype %s %s\n", structName, typeForValue(iresult, structName, tags, subStructMap, convertFloats))
// 补上子结构体
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("package %s\ntype %s %s}", pkgName, structName, generateTypes(result, structName, tags, 0, subStructMap, convertFloats))
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) // 使用字段名字
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) // 使用字段名字
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) // 使用字段名字
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
}
}

View File

@ -0,0 +1,72 @@
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)
}

19
pkg/jy2struct/test.json Normal file
View File

@ -0,0 +1,19 @@
[
{
"name": "foo",
"age": 10,
"email": "foo@bar.com",
"companies": [
{
"name":"foo",
"address":"foo",
"position": "foo"
},
{
"name":"foo2",
"address":"foo2",
"position": "foo2"
}
]
}
]

20
pkg/jy2struct/test.yaml Normal file
View File

@ -0,0 +1,20 @@
serverName: "demo"
serverPort: 8080
runMode: "dev"
# jwt配置
jwt:
signingKey: "abcd"
expire: 86400 # 单位(秒)
# email配置
email:
sender: "foo@bar.com"
password: "1234"
# 日志配置
log:
level: "debug" # 输出日志级别 debug, info, warn, error默认是debug
format: "console" # 输出格式console或json默认是console
isSave: true # false:输出到终端true:输出到文件默认是false

View File

@ -2,8 +2,11 @@ package nacos
import (
"context"
"github.com/zhufuyi/sponge/pkg/registry"
"testing"
"github.com/zhufuyi/sponge/pkg/registry"
"github.com/stretchr/testify/assert"
)
func newNacosRegistry() *Registry {
@ -32,3 +35,25 @@ func TestRegistry(t *testing.T) {
_, err = r.Watch(context.Background(), "foo")
t.Log(err)
}
func TestRegistry_Register(t *testing.T) {
r := newNacosRegistry()
instance := registry.NewServiceInstance("", []string{"grpc://127.0.0.1:8282"})
err := r.Register(context.Background(), instance)
assert.Error(t, err)
instance = registry.NewServiceInstance("foo", []string{"grpc://127.0.0.1:8282"},
registry.WithMetadata(map[string]string{
"foo2": "bar2",
}))
err = r.Register(context.Background(), instance)
assert.Error(t, err)
instance = registry.NewServiceInstance("foo", []string{"127.0.0.1:port"})
err = r.Register(context.Background(), instance)
assert.Error(t, err)
instance = registry.NewServiceInstance("foo", []string{"127.0.0.1"})
err = r.Register(context.Background(), instance)
assert.Error(t, err)
}

53
pkg/replacer/README.md Normal file
View File

@ -0,0 +1,53 @@
## replace
一个替换目录下文件内容库支持本地目录下文件和通过embed嵌入目录文件替换。
<br>
### 安装
> go get -u github.com/zhufuyi/pkg/replacer
<br>
### 使用示例
```go
//go:embed dir
var fs embed.FS
func demo(){
//r, err := replacer.New("dir")
//if err != nil {
// panic(err)
//}
r, err := replacer.NewWithFS("dir", fs)
if err != nil {
panic(err)
}
ignoreFiles := []string{}
fields := []replacer.Field{
{
Old: "1234",
New: "8080",
},
{
Old: "abcde",
New: "hello",
IsCaseSensitive: true, // abcde-->hello, Abcde-->Hello
},
}
r.SetSubDirs(subPaths...) // 只处理指定子目录,优先级最高
r.SetIgnoreDirs(ignoreDirs...) // 指定子目录下忽略处理的目录
r.SetIgnoreFiles(ignoreFiles...) // 指定子目录下忽略处理的文件
r.SetReplacementFields(fields) // 设置替换文本
r.SetOutPath("", "test") // 设置输出目录,如果为空,根据名称和时间生成文件输出文件夹
err = r.SaveFiles() // 保存替换后文件
if err != nil {
panic(err)
}
fmt.Printf("save files successfully, out = %s\n", replacer.GetOutPath())
}
```

383
pkg/replacer/replacer.go Normal file
View File

@ -0,0 +1,383 @@
package replacer
import (
"bytes"
"embed"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/zhufuyi/sponge/pkg/gofile"
)
var _ Replacer = (*replacerInfo)(nil)
// Replacer 接口
type Replacer interface {
SetReplacementFields(fields []Field)
SetIgnoreFiles(filenames ...string)
SetIgnoreSubDirs(dirs ...string)
SetSubDirs(subDirs ...string)
SetOutputDir(absDir string, name ...string) error
GetOutputDir() string
GetSourcePath() string
SaveFiles() error
ReadFile(filename string) ([]byte, error)
}
// replacerInfo replacer信息
type replacerInfo struct {
path string // 模板目录或文件
fs embed.FS // 模板目录对应二进制对象
isActual bool // fs字段是否来源实际路径如果为true使用io操作文件如果为false使用fs操作文件
files []string // 模板文件列表
ignoreFiles []string // 忽略替换的文件列表
ignoreDirs []string // 忽略处理的子目录
replacementFields []Field // 从模板文件转为新文件需要替换的字符
outPath string // 输出替换后文件存放目录路径
}
// New 根据指定路径创建replacer
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 根据嵌入的路径创建replacer
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 替换字段信息
type Field struct {
Old string // 模板字段
New string // 新字段
IsCaseSensitive bool // 第一个字母是否区分大小写
}
// SetReplacementFields 设置替换字段old字符尽量不要存在包含关系如果存在在设置Field时注意先后顺序
func (r *replacerInfo) SetReplacementFields(fields []Field) {
var newFields []Field
for _, field := range fields {
if field.IsCaseSensitive && isFirstAlphabet(field.Old) { // 拆分首字母大小写两个字段
newFields = append(newFields,
Field{ // 把第一个字母转为大写
Old: strings.ToUpper(field.Old[:1]) + field.Old[1:],
New: strings.ToUpper(field.New[:1]) + field.New[1:],
},
Field{ // 把第一个字母转为小写
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
}
// SetSubDirs 设置处理指定子目录,其他目录下文件忽略处理
func (r *replacerInfo) SetSubDirs(subDirs ...string) {
if len(subDirs) == 0 {
return
}
subDirs = r.covertPathsDelimiter(subDirs...)
var files []string
isExistFile := make(map[string]struct{})
for _, file := range r.files {
for _, dir := range subDirs {
if isSubPath(file, dir) {
// 避免重复文件
if _, ok := isExistFile[file]; ok {
continue
} else {
isExistFile[file] = struct{}{}
}
files = append(files, file)
}
}
}
if len(files) == 0 {
return
}
r.files = files
}
// SetIgnoreFiles 设置忽略处理的文件
func (r *replacerInfo) SetIgnoreFiles(filenames ...string) {
r.ignoreFiles = append(r.ignoreFiles, filenames...)
}
// SetIgnoreSubDirs 设置忽略处理的子目录
func (r *replacerInfo) SetIgnoreSubDirs(dirs ...string) {
dirs = r.covertPathsDelimiter(dirs...)
r.ignoreDirs = append(r.ignoreDirs, dirs...)
}
// SetOutputDir 设置输出目录优先使用absPath如果absPath为空自动在当前目录根据参数name名称生成输出目录
func (r *replacerInfo) SetOutputDir(absPath string, name ...string) error {
// 使用指定输出目录
if absPath != "" {
abs, err := filepath.Abs(absPath)
if err != nil {
return err
}
r.outPath = abs
return nil
}
// 使用当前目录
subPath := ""
if len(name) > 0 && name[0] != "" {
subPath = name[0]
}
r.outPath = gofile.GetRunPath() + gofile.GetPathDelimiter() + subPath + "_" + time.Now().Format("150405")
return nil
}
// GetOutputDir 获取输出目录
func (r *replacerInfo) GetOutputDir() string {
return r.outPath
}
// GetSourcePath 获取源路径
func (r *replacerInfo) GetSourcePath() string {
return r.path
}
// ReadFile 读取文件内容
func (r *replacerInfo) ReadFile(filename string) ([]byte, error) {
filename = r.covertPathDelimiter(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 导出文件
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
}
// 从二进制读取模板文件内容使用embed.FS如果要从指定目录读取使用os.ReadFile
var data []byte
var err error
if r.isActual {
data, err = os.ReadFile(file)
} else {
data, err = r.fs.ReadFile(file)
}
if err != nil {
return err
}
// 替换文本内容
for _, field := range r.replacementFields {
data = bytes.ReplaceAll(data, []byte(field.Old), []byte(field.New))
}
// 获取新文件路径
newFilePath := r.getNewFilePath(file)
dir, filename := filepath.Split(newFilePath)
// 替换文件名和文件夹名
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 {
return fmt.Errorf("existing files detected\n %s\nCode generation has been cancelled\n", strings.Join(existFiles, "\n "))
}
for file, data := range writeData {
// 保存文件
err := saveToNewFile(file, data)
if err != nil {
return err
}
}
return nil
}
func (r *replacerInfo) isIgnoreFile(file string) bool {
isIgnore := false
_, filename := filepath.Split(file)
for _, v := range r.ignoreFiles {
if filename == 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 (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)
}
if gofile.IsWindows() {
newFilePath = strings.ReplaceAll(newFilePath, "/", "\\")
}
return newFilePath
}
// 如果是windows转换路径分割符
func (r *replacerInfo) covertPathDelimiter(filePath string) string {
if r.isActual && gofile.IsWindows() {
filePath = strings.ReplaceAll(filePath, "/", "\\")
}
return filePath
}
// 如果是windows批量转换路径分割符
func (r *replacerInfo) covertPathsDelimiter(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 {
// 创建目录
dir, _ := filepath.Split(filePath)
err := os.MkdirAll(dir, 0666)
if err != nil {
return err
}
// 保存文件
err = os.WriteFile(filePath, data, 0666)
if err != nil {
return err
}
return nil
}
// 遍历嵌入的目录下所有文件,返回文件的绝对路径
func listFiles(path string, fs embed.FS) ([]string, error) {
files := []string{}
err := walkDir(path, &files, fs)
return files, err
}
// 通过迭代方式遍历嵌入的目录
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
}
// 判断字符串第一个字符是字母
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)
}

View File

@ -0,0 +1,103 @@
package replacer
import (
"embed"
"github.com/stretchr/testify/assert"
"testing"
)
//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"}
ignoreDirs := []string{"testDir/ignore"}
ignoreFiles := []string{"test.txt"}
fields := []Field{
{
Old: "1234",
New: "....",
},
{
Old: "abcdef",
New: "hello_",
IsCaseSensitive: true,
},
}
r.SetSubDirs(subDirs...) // 只处理指定子目录,为空时表示指定全部文件
r.SetIgnoreFiles(ignoreDirs...) // 忽略替换目录
r.SetIgnoreFiles(ignoreFiles...) // 忽略替换文件
r.SetReplacementFields(fields) // 设置替换文本
_ = r.SetOutputDir("", tt.name+"_test") // 设置输出目录和名称
_, err := r.ReadFile("replace.txt")
assert.NoError(t, err)
err = r.SaveFiles() // 保存替换后文件
if (err != nil) != tt.wantErr {
t.Errorf("SaveFiles() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Logf("save files successfully, out = %s", r.GetOutputDir())
})
}
}
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.SetIgnoreFiles()
r.SetSubDirs()
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)
}

View File

@ -0,0 +1 @@
ignore

View File

@ -0,0 +1 @@
change file name

View File

@ -0,0 +1,3 @@
1234567890
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ

View File

@ -0,0 +1,3 @@
test1
to do replace

63
pkg/sql2code/README.md Normal file
View File

@ -0,0 +1,63 @@
## sql2code
根据sql生成不同用途代码支持生成json、gorm model、dao、handler代码sql可以从参数、文件、db三种方式获取优先从高到低。
<br>
### 安装
> go get -u github.com/zhufuyi/pkg/sql2code
<br>
### 使用示例
主要设置参数
```go
type Args struct {
SQL string // DDL sql
DDLFile string // 读取文件的DDL sql
DBDsn string // 从db获取表的DDL sql
DBTable string
Package string // 生成字段的包名(只有model类型有效)
GormType bool // gorm type
JSONTag bool // 是否包括json tag
JSONNamedType int // json命名类型0:和列名一致,其他值表示驼峰
IsEmbed bool // 是否嵌入gorm.Model
CodeType string // 指定生成代码用途支持4中类型分别是 model(默认), json, dao, handler
}
```
<br>
生成代码示例:
```go
// 生成gorm model 代码
code, err := sql2code.GenerateOne(&sql2code.Args{
SQL: sqlData, // 来源于sql语句
// DDLFile: "user.sql", // 来源于sql文件
// DBDsn: "root:123456@(127.0.0.1:3306)/account"
// DBTable "user"
GormType: true,
JSONTag: true,
IsEmbed: true,
CodeType: "model"
})
// 生成json、model、dao、handler代码
codes, err := sql2code.Generate(&sql2code.Args{
SQL: sqlData, // 来源于sql语句
// DDLFile: "user.sql", // 来源于sql文件
// DBDsn: "root:123456@(127.0.0.1:3306)/account"
// DBTable "user"
GormType: true,
JSONTag: true,
IsEmbed: true,
CodeType: "model"
})
```

View File

@ -0,0 +1,36 @@
package parser
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" //nolint
)
// GetTableInfo get table info from mysql
func GetTableInfo(dsn, tableName string) (string, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return "", fmt.Errorf("connect mysql 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
}

Some files were not shown because too many files have changed in this diff Show More