This commit is contained in:
新亮
2021-01-16 14:45:42 +08:00
parent f96d33fdef
commit 2466dfcbfc
33 changed files with 753 additions and 300 deletions

View File

@@ -1,4 +1,4 @@
## go-gin-api
## go-gin-api ![go report](https://goreportcard.com/badge/github.com/xinliangnote/go-gin-api)
[![GitHub stars](https://img.shields.io/github/stars/xinliangnote/go-gin-api)](https://github.com/xinliangnote/go-gin-api/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/xinliangnote/go-gin-api)](https://github.com/xinliangnote/go-gin-api/network/members)
@@ -49,23 +49,49 @@
- [x] [Gin](https://github.com/gin-gonic/gin) 支持优雅关闭
- [x] 配置文件解析库 [Viper](https://github.com/spf13/viper)
- [x] 文档使用 [Swagger](https://swagger.io/) 生成
- [x] 性能分析使用 [pprof](github.com/gin-contrib/pprof)
- [x] [zap](go.uber.org/zap) 日志记录
- [x] [rate](golang.org/x/time/rate) 限流
- [x] [token] 基于[JWT](github.com/dgrijalva/jwt-go) 身份认证
- [x] 性能分析使用 [pprof](https://github.com/gin-contrib/pprof)
- [x] [zap](https://go.uber.org/zap) 日志记录
- [x] [rate](https://golang.org/x/time/rate) 限流
- [x] [token] 基于[JWT](https://github.com/dgrijalva/jwt-go) 身份认证
- [x] [notify] 异常捕获并进行邮件告警
- [x] [trace] 开发调试的辅助工具
- [x] 支持设置 trace_id
- [x] 支持设置 request 信息
- [x] 支持设置 response 信息
- [x] 支持设置 third_party_requests 三方请求信息
- [x] 支持设置 debugs 打印调试信息
- [x] 支持设置 sqls 执行 SQL 信息
- [x] 可记录 cost_seconds 执行时长
- [x] [errno] 统一定义错误码
- [x] [env] 支持 FAT、UAT、PRO 环境
- [x] [aes] AES 对称加密
- [x] 支持密码分组链模式CBC
- [x] [rsa] RSA 非对称加密
- [x] 数据库组件使用 [GORM V2](gorm.io/gorm)
- [x] 数据库组件使用 [GORM V2](https://gorm.io/gorm)
- [x] 自带连接池
- [x] Redis 组件使用 [go-redis](https://github.com/go-redis/redis)
- [x] 自带连接池
- [ ] MongoDB
- [ ] Prometheus
- [x] 使用 [Prometheus Client](https://github.com/prometheus/client_golang/prometheus)
- [x] 已设置计数器Counter指标
- [x] 已设置直方图Histogram指标
- [ ] 任务调度
- [ ] gRPC
- [ ] ...
- [ ] ID 生成器
- [x] [httpclient] HTTP 请求包
- [x] 支持设置 TTL 一次请求的最长执行时间
- [x] 支持设置 Header 信息
- [x] 支持设置 Trace 信息
- [x] 支持设置 Logger 信息
- [x] 支持设置 Mock 数据
- [x] 支持设置 OnFailedAlarm 失败
- [x] 可设置 alarmTitle 告警标题
- [x] 可设置 alarmObject 告警方式(邮件/短信/微信)
- [x] 可设置 alarmVerify 定义符合告警的验证规则
- [x] 支持设置 OnFailedRetry 失败重试
- [x] 可设置 retryTimes 重试次数
- [x] 可设置 retryDelay 重试前延迟等待时间
- [x] 可设置 retryVerify 定义符合重试的验证规则
## Quick start

View File

@@ -31,7 +31,7 @@ func main() {
loggers, err := logger.NewJSONLogger(
logger.WithField("domain", configs.ProjectName()),
logger.WithTimeLayout("2006-01-02 15:04:05"),
logger.WithFileP(fmt.Sprintf("./logs/%s.log", configs.ProjectName())),
logger.WithFileP(fmt.Sprintf("./logs/%s-access.log", configs.ProjectName())),
)
if err != nil {
panic(err)

View File

@@ -0,0 +1,12 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["127.0.0.1:9090"]
- job_name: "go_app_server"
static_configs:
- targets:
- "127.0.0.1:9999"

3
go.mod
View File

@@ -11,16 +11,15 @@ require (
github.com/go-openapi/spec v0.20.0 // indirect
github.com/go-redis/redis/v7 v7.4.0
github.com/google/go-cmp v0.5.4 // indirect
github.com/jinzhu/gorm v1.9.16
github.com/onsi/ginkgo v1.14.2 // indirect
github.com/onsi/gomega v1.10.4 // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v0.9.3
github.com/rs/cors v1.7.0
github.com/spf13/cast v1.3.0
github.com/spf13/viper v1.7.1
github.com/swaggo/gin-swagger v1.3.0
github.com/swaggo/swag v1.7.0
github.com/tidwall/gjson v1.6.6
go.uber.org/multierr v1.5.0
go.uber.org/zap v1.16.0
golang.org/x/mod v0.4.0 // indirect

27
go.sum
View File

@@ -17,7 +17,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -27,7 +26,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -50,13 +48,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -112,8 +106,6 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -174,12 +166,8 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
@@ -206,8 +194,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -222,8 +208,6 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -321,12 +305,6 @@ github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
github.com/tidwall/gjson v1.6.6 h1:Gh0D/kZV+L9rcuE2hE8Hn2dTYe2L6j6SKwcPlKpXAcs=
github.com/tidwall/gjson v1.6.6/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
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=
@@ -359,11 +337,9 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -388,7 +364,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -408,8 +383,6 @@ golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=

View File

@@ -23,6 +23,7 @@ var (
ErrUserCreate = errno.NewError(20102, "创建用户失败")
ErrUserUpdate = errno.NewError(20103, "更新用户失败")
ErrUserSearch = errno.NewError(20104, "查询用户失败")
ErrUserHTTP = errno.NewError(20105, "调用他方接口失败")
// ...
)

View File

@@ -1,12 +1,11 @@
package demo
import (
"net/url"
"time"
"github.com/xinliangnote/go-gin-api/internal/api/code"
"github.com/xinliangnote/go-gin-api/internal/api/repository/third_party_request/go_gin_api_repo"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/jsonparse"
"github.com/xinliangnote/go-gin-api/pkg/httpclient"
"go.uber.org/zap"
@@ -113,40 +112,41 @@ func (d *Demo) User() core.HandlerFunc {
return
}
body1, err1 := httpclient.Get("http://127.0.0.1:9999/demo/get/"+req.Name, nil,
httpclient.WithTTL(time.Second*2),
res1, err := go_gin_api_repo.DemoGet(req.Name,
httpclient.WithTTL(time.Second*5),
httpclient.WithTrace(c.Trace()),
httpclient.WithLogger(c.Logger()),
httpclient.WithHeader("Authorization", c.GetHeader("Authorization")),
httpclient.WithOnFailedRetry(3, time.Second*1, go_gin_api_repo.DemoGetRetryVerify),
)
if err1 != nil {
d.logger.Error("get [demo/get] err", zap.Error(err1))
if err != nil {
d.logger.Error("get [demo/get] err", zap.Error(err))
c.SetPayload(code.ErrUserHTTP)
}
params := url.Values{}
params.Set("name", "Jack")
body2, err2 := httpclient.PostForm("http://127.0.0.1:9999/demo/post", params,
httpclient.WithTTL(time.Second*2),
res2, err := go_gin_api_repo.DemoPost("Jack",
httpclient.WithTTL(time.Second*5),
httpclient.WithTrace(c.Trace()),
httpclient.WithLogger(c.Logger()),
httpclient.WithHeader("Authorization", c.GetHeader("Authorization")),
httpclient.WithOnFailedRetry(3, time.Second*1, go_gin_api_repo.DemoPostRetryVerify),
)
if err2 != nil {
d.logger.Error("post [demo/post] err", zap.Error(err2))
if err != nil {
d.logger.Error("post [demo/post] err", zap.Error(err))
c.SetPayload(code.ErrUserHTTP)
}
data := &response{}
if err1 == nil && err2 == nil {
data = &response{
{
Name: jsonparse.Get(string(body1), "data.name").(string),
Job: jsonparse.Get(string(body1), "data.job").(string),
},
{
Name: jsonparse.Get(string(body2), "data.name").(string),
Job: jsonparse.Get(string(body2), "data.job").(string),
},
}
data := &response{
{
Name: res1.Data.Name,
Job: res1.Data.Job,
},
{
Name: res2.Data.Name,
Job: res2.Data.Job,
},
}
c.SetPayload(code.OK.WithData(data))
}

View File

@@ -14,7 +14,6 @@ import (
const (
callBackBeforeName = "core:before"
callBackAfterName = "core:after"
coreContext = "_core_context"
startTime = "_start_time"
)
@@ -24,46 +23,6 @@ func (op *TracePlugin) Name() string {
return "tracePlugin"
}
func before(db *gorm.DB) {
db.InstanceSet(coreContext, db.Statement.Context)
db.InstanceSet(startTime, time.Now())
return
}
func after(db *gorm.DB) {
_ctx, isExist := db.InstanceGet(coreContext)
if !isExist {
return
}
ctx, ok := _ctx.(core.Context)
if !ok {
return
}
_ts, isExist := db.InstanceGet(startTime)
if !isExist {
return
}
ts, ok := _ts.(time.Time)
if !ok {
return
}
sql := db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...)
sqlInfo := new(trace.SQL)
sqlInfo.Time = time_parse.CSTLayoutString()
sqlInfo.SQL = sql
sqlInfo.Src = utils.FileWithLineNum()
sqlInfo.Rows = db.Statement.RowsAffected
sqlInfo.Duration = time.Since(ts).Seconds()
ctx.Trace().AppendSQL(sqlInfo)
return
}
func (op *TracePlugin) Initialize(db *gorm.DB) (err error) {
// 开始前
_ = db.Callback().Create().Before("gorm:before_create").Register(callBackBeforeName, before)
@@ -84,3 +43,38 @@ func (op *TracePlugin) Initialize(db *gorm.DB) (err error) {
}
var _ gorm.Plugin = &TracePlugin{}
func before(db *gorm.DB) {
db.InstanceSet(startTime, time.Now())
return
}
func after(db *gorm.DB) {
_ctx := db.Statement.Context
ctx, ok := _ctx.(core.Context)
if !ok {
return
}
_ts, isExist := db.InstanceGet(startTime)
if !isExist {
return
}
ts, ok := _ts.(time.Time)
if !ok {
return
}
sql := db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...)
sqlInfo := new(trace.SQL)
sqlInfo.Timestamp = time_parse.CSTLayoutString()
sqlInfo.SQL = sql
sqlInfo.Stack = utils.FileWithLineNum()
sqlInfo.Rows = db.Statement.RowsAffected
sqlInfo.CostSeconds = time.Since(ts).Seconds()
ctx.Trace().AppendSQL(sqlInfo)
return
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/xinliangnote/go-gin-api/internal/api/repository/db_repo"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
@@ -43,7 +42,7 @@ func (u *userRepo) Create(ctx core.Context, user user_model.UserDemo) (id uint,
func (u *userRepo) getUserByID(ctx core.Context, id uint) (*user_model.UserDemo, error) {
data := new(user_model.UserDemo)
err := u.db.GetDbR().WithContext(ctx).First(data, id).Error
if err != nil && err != gorm.ErrRecordNotFound {
if err != nil {
return nil, errors.Wrap(err, "[user_demo] get user data err")
}
return data, nil
@@ -64,7 +63,7 @@ func (u *userRepo) GetUserByUserName(ctx core.Context, username string) (*user_m
Select([]string{"id", "user_name", "nick_name", "mobile"}).
Where("user_name = ?", username).
First(data).Error
if err != nil && err != gorm.ErrRecordNotFound {
if err != nil {
return nil, errors.Wrap(err, "[user_demo] get user data err")
}
return data, nil

View File

@@ -0,0 +1,24 @@
package third_party_request
import (
"github.com/xinliangnote/go-gin-api/pkg/httpclient"
"github.com/xinliangnote/go-gin-api/pkg/mail"
)
// 实现 AlarmObject 告警
var _ httpclient.AlarmObject = (*AlarmEmail)(nil)
type AlarmEmail struct{}
func (a *AlarmEmail) Send(subject, body string) error {
options := &mail.Options{
MailHost: "smtp.163.com",
MailPort: 465,
MailUser: "xx@163.com",
MailPass: "",
MailTo: "",
Subject: subject,
Body: body,
}
return mail.Send(options)
}

View File

@@ -0,0 +1,109 @@
package go_gin_api_repo
import (
"encoding/json"
"fmt"
"net/url"
"github.com/xinliangnote/go-gin-api/pkg/httpclient"
"github.com/pkg/errors"
)
type demoGetResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Name string `json:"name"`
Job string `json:"job"`
} `json:"data"`
ID string `json:"id"`
}
type demoPostResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Name string `json:"name"`
Job string `json:"job"`
} `json:"data"`
ID string `json:"id"`
}
func DemoGet(name string, opts ...httpclient.Option) (res *demoGetResponse, err error) {
api := "http://127.0.0.1:9999/demo/get/" + name
body, err := httpclient.Get(api, nil, opts...)
if err != nil {
return nil, err
}
res = new(demoGetResponse)
err = json.Unmarshal(body, res)
if err != nil {
return nil, errors.Wrap(err, "DemoGet json unmarshal error")
}
if res.Code != 1 {
return nil, errors.New(fmt.Sprintf("code err: %d-%s", res.Code, res.Msg))
}
return res, nil
}
func DemoGetRetryVerify(body []byte) (shouldRetry bool) {
if len(body) == 0 {
return true
}
type Response struct {
Code int `json:"code"`
}
resp := new(Response)
if err := json.Unmarshal(body, resp); err != nil {
return true
}
// 例如 无需重试的 code 码code !=1 需要重试
successCode := 1
return resp.Code != successCode
}
func DemoPost(name string, opts ...httpclient.Option) (res *demoPostResponse, err error) {
api := "http://127.0.0.1:9999/demo/post"
params := url.Values{}
params.Set("name", name)
body, err := httpclient.PostForm(api, params, opts...)
if err != nil {
return nil, err
}
res = new(demoPostResponse)
err = json.Unmarshal(body, res)
if err != nil {
return nil, errors.Wrap(err, "DemoPost json unmarshal error")
}
if res.Code != 1 {
return nil, errors.New(fmt.Sprintf("code err: %d-%s", res.Code, res.Msg))
}
return res, nil
}
func DemoPostRetryVerify(body []byte) (shouldRetry bool) {
if len(body) == 0 {
return true
}
type Response struct {
Code int `json:"code"`
}
resp := new(Response)
if err := json.Unmarshal(body, resp); err != nil {
return true
}
// 例如 无需重试的 code 码code !=1 需要重试
successCode := 1
return resp.Code != successCode
}

View File

@@ -0,0 +1,25 @@
package go_gin_api_repo
import "encoding/json"
func MockDemoGet() (body []byte) {
res := new(demoGetResponse)
res.Code = 1
res.Msg = "ok"
res.Data.Name = "AA"
res.Data.Job = "AA_JOB"
body, _ = json.Marshal(res)
return body
}
func MockDemoPost() (body []byte) {
res := new(demoPostResponse)
res.Code = 1
res.Msg = "ok"
res.Data.Name = "BB"
res.Data.Job = "BB_JOB"
body, _ = json.Marshal(res)
return body
}

View File

@@ -0,0 +1,80 @@
package go_gin_api_repo
import (
"encoding/json"
"testing"
"time"
"github.com/xinliangnote/go-gin-api/internal/api/repository/third_party_request"
"github.com/xinliangnote/go-gin-api/pkg/httpclient"
)
var authorization = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjEwLCJVc2VyTmFtZSI6IjEyMyIsImV4cCI6MTYxMDcxODA3NCwiaWF0IjoxNjEwNjMxNjc0LCJpc3MiOiJnby1naW4tYXBpIiwibmJmIjoxNjEwNjMxNjc0fQ.S3T4MaIaz3XjkbJ-xkMDkwzuZ_jfZ8ZRf4cPMz0oXBE"
func TestDemoGet(t *testing.T) {
res, err := DemoGet("Tom",
httpclient.WithTTL(time.Second*5),
//httpclient.WithTrace(ctx.Trace()),
//httpclient.WithLogger(ctx.Logger()),
httpclient.WithHeader("Authorization", authorization),
httpclient.WithOnFailedRetry(3, time.Second*1, retryVerify),
httpclient.WithOnFailedAlarm("接口异常", new(third_party_request.AlarmEmail), alarmVerify),
//httpclient.WithMock(MockDemoGet),
)
if err != nil {
t.Log("get [demo/get] err", err)
}
t.Log(res)
}
func TestDemoPost(t *testing.T) {
res, err := DemoPost("Jack",
httpclient.WithTTL(time.Second*5),
//httpclient.WithTrace(ctx.Trace()),
//httpclient.WithLogger(ctx.Logger()),
httpclient.WithHeader("Authorization", authorization),
httpclient.WithMock(MockDemoPost),
)
if err != nil {
t.Log("post [demo/post] err", err)
}
t.Log(res)
}
// 设置重试规则
func retryVerify(body []byte) (shouldRetry bool) {
if len(body) == 0 {
return true
}
type Response struct {
Code int `json:"code"`
}
resp := new(Response)
if err := json.Unmarshal(body, resp); err != nil {
return true
}
return resp.Code != 1
}
// 设置告警规则
func alarmVerify(body []byte) (shouldRetry bool) {
if len(body) == 0 {
return true
}
type Response struct {
Code int `json:"code"`
}
resp := new(Response)
if err := json.Unmarshal(body, resp); err != nil {
return true
}
return resp.Code != 1
}

View File

@@ -38,17 +38,17 @@ func NewHTTPMux(logger *zap.Logger, db db_repo.Repo, cache cache_repo.Repo) (cor
{
u.POST("/login", userHandler.Login())
u.POST("/create", userHandler.Create())
u.GET("/info/:username", userHandler.Detail())
u.GET("/info/:username", core.AliasForRecordMetrics("/user/info"), userHandler.Detail())
u.POST("/update", userHandler.UpdateNickNameByID())
}
d := mux.Group("/demo", core.WrapAuthHandler(auth.AuthHandler)) //使用 auth 验证
{
d.GET("user/:name", demoHandler.User())
d.GET("user/:name", core.AliasForRecordMetrics("/demo/user"), demoHandler.User())
// 模拟数据
d.GET("get/:name", demoHandler.Get(), core.DisableTrace)
d.POST("post", demoHandler.Post(), core.DisableTrace)
d.GET("get/:name", core.AliasForRecordMetrics("/demo/get"), demoHandler.Get())
d.POST("post", demoHandler.Post())
}
return mux, nil

View File

@@ -35,7 +35,7 @@ const _UI = `
╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝
`
const _MaxBurstSize = 100
const _MaxBurstSize = 100000
type Option func(*option)
@@ -54,7 +54,7 @@ type OnPanicNotify func(ctx Context, err interface{}, stackInfo string)
// RecordMetrics 记录prometheus指标用
// 如果使用AliasForRecordMetrics配置了别名uri将被替换为别名。
type RecordMetrics func(method, uri string, success bool, httpCode, businessCode int, costSeconds float64)
type RecordMetrics func(method, uri string, success bool, httpCode, businessCode int, costSeconds float64, traceId string)
// WithDisablePProf 禁用 pprof
func WithDisablePProf() Option {
@@ -352,6 +352,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
businessCode int
businessCodeMsg string
abortErr error
traceId string
)
if ctx.IsAborted() {
@@ -371,6 +372,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
if x := context.Trace(); x != nil {
context.SetHeader(trace.Header, x.ID())
response.WithID(x.ID())
traceId = x.ID()
} else {
response.WithID("")
}
@@ -385,7 +387,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
uri = alias
}
opt.recordMetrics(context.Method(), uri, !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK, ctx.Writer.Status(), businessCode, time.Since(ts).Seconds())
opt.recordMetrics(context.Method(), uri, !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK, ctx.Writer.Status(), businessCode, time.Since(ts).Seconds(), traceId)
}
var t *trace.Trace
@@ -416,7 +418,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
t.Success = !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK
t.CostSeconds = time.Since(ts).Seconds()
logger.Info("interceptor", zap.Any("trace", t))
logger.Info("core-interceptor", zap.Any("trace", t))
}()
ctx.Next()
@@ -429,8 +431,7 @@ func New(logger *zap.Logger, options ...Option) (Mux, error) {
defer releaseContext(context)
if !limiter.Allow() {
context.SetPayload(code.ErrManyRequest)
ctx.Abort()
context.AbortWithError(code.ErrManyRequest)
return
}
ctx.Next()

View File

@@ -1,7 +0,0 @@
package jsonparse
import "github.com/tidwall/gjson"
func Get(json, path string) interface{} {
return gjson.Get(json, path).Value()
}

View File

@@ -1,6 +1,45 @@
package metrics
// RecordMetrics 记录指标
func RecordMetrics(method, uri string, success bool, httpCode, businessCode int, costSeconds float64) {
//fmt.Printf(">>>>>>>Metrics\nmethod:%s\nuri:%s\nsuccess:%t\nhttp code:%d\nbusiness code:%d\ncost seconds:%.9f\n<<<<<<<\n", method, uri, success, httpCode, businessCode, costSeconds)
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/cast"
)
// requestsCounter 定义计数器Counter
var requestsCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "api_requests_total",
},
[]string{"method", "path"},
)
// httpDurationsHistogram 定义累积直方图Histogram
var httpDurationsHistogram = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "api_http_durations_histogram_seconds",
Buckets: []float64{0.01, 0.02, 0.03},
},
[]string{"method", "path", "success", "http_code", "business_code", "cost_seconds", "trace_id"},
)
func init() {
prometheus.MustRegister(requestsCounter, httpDurationsHistogram)
}
// RecordMetrics 记录指标
func RecordMetrics(method, uri string, success bool, httpCode, businessCode int, costSeconds float64, traceId string) {
httpDurationsHistogram.With(prometheus.Labels{
"method": method,
"path": uri,
"success": cast.ToString(success),
"http_code": cast.ToString(httpCode),
"business_code": cast.ToString(businessCode),
"cost_seconds": cast.ToString(costSeconds),
"trace_id": traceId,
}).Observe(costSeconds)
requestsCounter.With(prometheus.Labels{
"method": method,
"path": uri,
}).Add(1)
}

View File

@@ -3,7 +3,7 @@ package notify
import (
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/internal/pkg/notify/mail"
"github.com/xinliangnote/go-gin-api/pkg/mail"
"go.uber.org/zap"
)
@@ -16,7 +16,7 @@ func OnPanicNotify(ctx core.Context, err interface{}, stackInfo string) {
return
}
subject, body, htmlErr := mail.NewPanicHTMLEmail(ctx.Method(), ctx.Host(), ctx.URI(), ctx.Trace().ID(), err, stackInfo)
subject, body, htmlErr := NewPanicHTMLEmail(ctx.Method(), ctx.Host(), ctx.URI(), ctx.Trace().ID(), err, stackInfo)
if htmlErr != nil {
ctx.Logger().Error("NewPanicHTMLEmail error", zap.Error(htmlErr))
return

View File

@@ -1,4 +1,4 @@
package mail
package notify
import (
"bytes"
@@ -6,7 +6,7 @@ import (
"html/template"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/notify/mail/templates"
"github.com/xinliangnote/go-gin-api/internal/pkg/notify/templates"
)
// NewPanicHTMLEmail 发送系统异常邮件 html

View File

@@ -49,9 +49,9 @@
执行的 SQL 信息,多个 SQL 会记录多组数据。
- time时间格式2006-01-02 15:04:05
- src,文件地址和行号
- duration,执行时长,单位:秒
- timestamp时间格式2006-01-02 15:04:05
- stack,文件地址和行号
- cost_seconds,执行时长,单位:秒
- sqlSQL 语句
- rows_affected影响行数

View File

@@ -1,9 +1,9 @@
package trace
type SQL struct {
Time string `json:"time"` // 时间格式2006-01-02 15:04:05
Src string `json:"src"` // 文件地址和行号
Duration float64 `json:"duration"` // 执行时长,单位:秒
SQL string `json:"sql"` // SQL 语句
Rows int64 `json:"rows_affected"` // 影响行数
Timestamp string `json:"timestamp"` // 时间格式2006-01-02 15:04:05
Stack string `json:"stack"` // 文件地址和行号
CostSeconds float64 `json:"cost_seconds"` // 执行时长,单位:秒
SQL string `json:"sql"` // SQL 语句
Rows int64 `json:"rows_affected"` // 影响行数
}

29
pkg/httpclient/alarm.go Normal file
View File

@@ -0,0 +1,29 @@
package httpclient
import (
"bufio"
"bytes"
"go.uber.org/zap"
)
// Verify parse the body and verify that it is correct
type AlarmVerify func(body []byte) (shouldAlarm bool)
type AlarmObject interface {
Send(subject, body string) error
}
func onFailedAlarm(title string, raw []byte, logger *zap.Logger, alarmObject AlarmObject) {
buf := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(bytes.NewReader(raw))
for scanner.Scan() {
buf.WriteString(scanner.Text())
buf.WriteString(" <br/>")
}
if err := alarmObject.Send(title, buf.String()); err != nil && logger != nil {
logger.Error("calls failed alarm err", zap.Error(err))
}
}

View File

@@ -3,6 +3,7 @@ package httpclient
import (
"context"
"encoding/json"
"fmt"
"net/http"
httpURL "net/url"
"time"
@@ -15,40 +16,8 @@ import (
const (
// DefaultTTL 一次http请求最长执行1分钟
DefaultTTL = time.Minute
// DefaultRetryTimes 如果请求失败最多重试3次
DefaultRetryTimes = 3
// DefaultRetryDelay 在重试前延迟等待100毫秒
DefaultRetryDelay = time.Millisecond * 100
)
// TODO retry的code不一定正确缺失或者多余待实际使用中修改。
func shouldRetry(ctx context.Context, httpCode int) bool {
select {
case <-ctx.Done():
return false
default:
}
switch httpCode {
case
_StatusDoReqErr, // customize
_StatusReadRespErr, // customize
http.StatusRequestTimeout,
http.StatusLocked,
http.StatusTooEarly,
http.StatusTooManyRequests,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout:
return true
default:
return false
}
}
// Get get 请求
func Get(url string, form httpURL.Values, options ...Option) (body []byte, err error) {
return withoutBody(http.MethodGet, url, form, options...)
@@ -72,24 +41,26 @@ func withoutBody(method, url string, form httpURL.Values, options ...Option) (bo
ts := time.Now()
opt := newOption()
opt := getOption()
defer func() {
if opt.Trace != nil {
opt.Dialog.Success = err == nil
opt.Dialog.CostSeconds = time.Since(ts).Seconds()
opt.Trace.AppendDialog(opt.Dialog)
if opt.trace != nil {
opt.dialog.Success = err == nil
opt.dialog.CostSeconds = time.Since(ts).Seconds()
opt.trace.AppendDialog(opt.dialog)
}
releaseOption(opt)
}()
for _, f := range options {
f(opt)
}
opt.Header["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
if opt.Trace != nil {
opt.Header[trace.Header] = opt.Trace.ID()
opt.header["Content-Type"] = []string{"application/x-www-form-urlencoded; charset=utf-8"}
if opt.trace != nil {
opt.header[trace.Header] = []string{opt.trace.ID()}
}
ttl := opt.TTL
ttl := opt.ttl
if ttl <= 0 {
ttl = DefaultTTL
}
@@ -97,30 +68,70 @@ func withoutBody(method, url string, form httpURL.Values, options ...Option) (bo
ctx, cancel := context.WithTimeout(context.Background(), ttl)
defer cancel()
if opt.Dialog != nil {
if opt.dialog != nil {
decodedURL, _ := httpURL.QueryUnescape(url)
opt.Dialog.Request = &trace.Request{
opt.dialog.Request = &trace.Request{
TTL: ttl.String(),
Method: method,
DecodedURL: decodedURL,
Header: opt.Header,
Header: opt.header,
}
}
retryTimes := opt.RetryTimes
retryTimes := opt.retryTimes
if retryTimes <= 0 {
retryTimes = DefaultRetryTimes
}
retryDelay := opt.RetryDelay
retryDelay := opt.retryDelay
if retryDelay <= 0 {
retryDelay = DefaultRetryDelay
}
var httpCode int
defer func() {
if opt.alarmObject == nil {
return
}
if opt.alarmVerify != nil && !opt.alarmVerify(body) && err == nil {
return
}
info := &struct {
TraceID string `json:"trace_id"`
Request struct {
Method string `json:"method"`
URL string `json:"url"`
} `json:"request"`
Response struct {
HTTPCode int `json:"http_code"`
Body string `json:"body"`
} `json:"response"`
Error string `json:"error"`
}{}
if opt.trace != nil {
info.TraceID = opt.trace.ID()
}
info.Request.Method = method
info.Request.URL = url
info.Response.HTTPCode = httpCode
info.Response.Body = string(body)
info.Error = ""
if err != nil {
info.Error = fmt.Sprintf("%+v", err)
}
raw, _ := json.MarshalIndent(info, "", " ")
onFailedAlarm(opt.alarmTitle, raw, opt.logger, opt.alarmObject)
}()
for k := 0; k < retryTimes; k++ {
body, httpCode, err = doHTTP(ctx, method, url, nil, opt)
if shouldRetry(ctx, httpCode) {
if shouldRetry(ctx, httpCode) || (opt.retryVerify != nil && opt.retryVerify(body)) {
time.Sleep(retryDelay)
continue
}
@@ -170,24 +181,26 @@ func withFormBody(method, url string, form httpURL.Values, options ...Option) (b
ts := time.Now()
opt := newOption()
opt := getOption()
defer func() {
if opt.Trace != nil {
opt.Dialog.Success = err == nil
opt.Dialog.CostSeconds = time.Since(ts).Seconds()
opt.Trace.AppendDialog(opt.Dialog)
if opt.trace != nil {
opt.dialog.Success = err == nil
opt.dialog.CostSeconds = time.Since(ts).Seconds()
opt.trace.AppendDialog(opt.dialog)
}
releaseOption(opt)
}()
for _, f := range options {
f(opt)
}
opt.Header["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
if opt.Trace != nil {
opt.Header[trace.Header] = opt.Trace.ID()
opt.header["Content-Type"] = []string{"application/x-www-form-urlencoded; charset=utf-8"}
if opt.trace != nil {
opt.header[trace.Header] = []string{opt.trace.ID()}
}
ttl := opt.TTL
ttl := opt.ttl
if ttl <= 0 {
ttl = DefaultTTL
}
@@ -196,31 +209,71 @@ func withFormBody(method, url string, form httpURL.Values, options ...Option) (b
defer cancel()
formValue := form.Encode()
if opt.Dialog != nil {
if opt.dialog != nil {
decodedURL, _ := httpURL.QueryUnescape(url)
opt.Dialog.Request = &trace.Request{
opt.dialog.Request = &trace.Request{
TTL: ttl.String(),
Method: method,
DecodedURL: decodedURL,
Header: opt.Header,
Header: opt.header,
Body: formValue,
}
}
retryTimes := opt.RetryTimes
retryTimes := opt.retryTimes
if retryTimes <= 0 {
retryTimes = DefaultRetryTimes
}
retryDelay := opt.RetryDelay
retryDelay := opt.retryDelay
if retryDelay <= 0 {
retryDelay = DefaultRetryDelay
}
var httpCode int
defer func() {
if opt.alarmObject == nil {
return
}
if opt.alarmVerify != nil && !opt.alarmVerify(body) && err == nil {
return
}
info := &struct {
TraceID string `json:"trace_id"`
Request struct {
Method string `json:"method"`
URL string `json:"url"`
} `json:"request"`
Response struct {
HTTPCode int `json:"http_code"`
Body string `json:"body"`
} `json:"response"`
Error string `json:"error"`
}{}
if opt.trace != nil {
info.TraceID = opt.trace.ID()
}
info.Request.Method = method
info.Request.URL = url
info.Response.HTTPCode = httpCode
info.Response.Body = string(body)
info.Error = ""
if err != nil {
info.Error = fmt.Sprintf("%+v", err)
}
raw, _ := json.MarshalIndent(info, "", " ")
onFailedAlarm(opt.alarmTitle, raw, opt.logger, opt.alarmObject)
}()
for k := 0; k < retryTimes; k++ {
body, httpCode, err = doHTTP(ctx, method, url, []byte(formValue), opt)
if shouldRetry(ctx, httpCode) {
if shouldRetry(ctx, httpCode) || (opt.retryVerify != nil && opt.retryVerify(body)) {
time.Sleep(retryDelay)
continue
}
@@ -240,24 +293,26 @@ func withJSONBody(method, url string, raw json.RawMessage, options ...Option) (b
ts := time.Now()
opt := newOption()
opt := getOption()
defer func() {
if opt.Trace != nil {
opt.Dialog.Success = err == nil
opt.Dialog.CostSeconds = time.Since(ts).Seconds()
opt.Trace.AppendDialog(opt.Dialog)
if opt.trace != nil {
opt.dialog.Success = err == nil
opt.dialog.CostSeconds = time.Since(ts).Seconds()
opt.trace.AppendDialog(opt.dialog)
}
releaseOption(opt)
}()
for _, f := range options {
f(opt)
}
opt.Header["Content-Type"] = "application/json; charset=utf-8"
if opt.Trace != nil {
opt.Header[trace.Header] = opt.Trace.ID()
opt.header["Content-Type"] = []string{"application/json; charset=utf-8"}
if opt.trace != nil {
opt.header[trace.Header] = []string{opt.trace.ID()}
}
ttl := opt.TTL
ttl := opt.ttl
if ttl <= 0 {
ttl = DefaultTTL
}
@@ -265,31 +320,71 @@ func withJSONBody(method, url string, raw json.RawMessage, options ...Option) (b
ctx, cancel := context.WithTimeout(context.Background(), ttl)
defer cancel()
if opt.Dialog != nil {
if opt.dialog != nil {
decodedURL, _ := httpURL.QueryUnescape(url)
opt.Dialog.Request = &trace.Request{
opt.dialog.Request = &trace.Request{
TTL: ttl.String(),
Method: method,
DecodedURL: decodedURL,
Header: opt.Header,
Header: opt.header,
Body: string(raw), // TODO unsafe
}
}
retryTimes := opt.RetryTimes
retryTimes := opt.retryTimes
if retryTimes <= 0 {
retryTimes = DefaultRetryTimes
}
retryDelay := opt.RetryDelay
retryDelay := opt.retryDelay
if retryDelay <= 0 {
retryDelay = DefaultRetryDelay
}
var httpCode int
defer func() {
if opt.alarmObject == nil {
return
}
if opt.alarmVerify != nil && !opt.alarmVerify(body) && err == nil {
return
}
info := &struct {
TraceID string `json:"trace_id"`
Request struct {
Method string `json:"method"`
URL string `json:"url"`
} `json:"request"`
Response struct {
HTTPCode int `json:"http_code"`
Body string `json:"body"`
} `json:"response"`
Error string `json:"error"`
}{}
if opt.trace != nil {
info.TraceID = opt.trace.ID()
}
info.Request.Method = method
info.Request.URL = url
info.Response.HTTPCode = httpCode
info.Response.Body = string(body)
info.Error = ""
if err != nil {
info.Error = fmt.Sprintf("%+v", err)
}
raw, _ := json.MarshalIndent(info, "", " ")
onFailedAlarm(opt.alarmTitle, raw, opt.logger, opt.alarmObject)
}()
for k := 0; k < retryTimes; k++ {
body, httpCode, err = doHTTP(ctx, method, url, raw, opt)
if shouldRetry(ctx, httpCode) {
if shouldRetry(ctx, httpCode) || (opt.retryVerify != nil && opt.retryVerify(body)) {
time.Sleep(retryDelay)
continue
}

View File

@@ -1,46 +0,0 @@
package httpclient
var _ ReplyErr = (*replyErr)(nil)
// ReplyErr 错误响应当resp.StatusCode != http.StatusOK时用来包装返回的httpcode和body。
type ReplyErr interface {
error
StatusCode() int
Body() []byte
}
type replyErr struct {
err error
statusCode int
body []byte
}
func (r *replyErr) Error() string {
return r.err.Error()
}
func (r *replyErr) StatusCode() int {
return r.statusCode
}
func (r *replyErr) Body() []byte {
return r.body
}
func newReplyErr(statusCode int, body []byte, err error) ReplyErr {
return &replyErr{
statusCode: statusCode,
body: body,
err: err,
}
}
// ToReplyErr 尝试将err转换为ReplyErr
func ToReplyErr(err error) (ReplyErr, bool) {
if err == nil {
return nil, false
}
e, ok := err.(ReplyErr)
return e, ok
}

View File

@@ -1,6 +1,7 @@
package httpclient
import (
"sync"
"time"
"github.com/xinliangnote/go-gin-api/internal/pkg/trace"
@@ -8,39 +9,75 @@ import (
"go.uber.org/zap"
)
var (
cache = &sync.Pool{
New: func() interface{} {
return &option{
header: make(map[string][]string),
}
},
}
)
// Trace 记录内部流转信息
type Trace = trace.T
// Mock 定义接口Mock数据
type Mock func() (body []byte)
// Option 自定义设置http请求
type Option func(*option)
type option struct {
TTL time.Duration
Header map[string]string
Trace *trace.Trace
Dialog *trace.Dialog
Logger *zap.Logger
RetryTimes int
RetryDelay time.Duration
ttl time.Duration
header map[string][]string
trace *trace.Trace
dialog *trace.Dialog
logger *zap.Logger
retryTimes int
retryDelay time.Duration
retryVerify RetryVerify
alarmTitle string
alarmObject AlarmObject
alarmVerify AlarmVerify
mock Mock
}
func newOption() *option {
return &option{
Header: make(map[string]string),
}
func (o *option) reset() {
o.ttl = 0
o.header = make(map[string][]string)
o.trace = nil
o.dialog = nil
o.logger = nil
o.retryTimes = 0
o.retryDelay = 0
o.retryVerify = nil
o.alarmTitle = ""
o.alarmObject = nil
o.alarmVerify = nil
o.mock = nil
}
func getOption() *option {
return cache.Get().(*option)
}
func releaseOption(opt *option) {
opt.reset()
cache.Put(opt)
}
// WithTTL 本次http请求最长执行时间
func WithTTL(ttl time.Duration) Option {
return func(opt *option) {
opt.TTL = ttl
opt.ttl = ttl
}
}
// WithHeader 设置http header可以调用多次设置多对key-value
func WithHeader(key, value string) Option {
return func(opt *option) {
opt.Header[key] = value
opt.header[key] = []string{value}
}
}
@@ -48,8 +85,8 @@ func WithHeader(key, value string) Option {
func WithTrace(t Trace) Option {
return func(opt *option) {
if t != nil {
opt.Trace = t.(*trace.Trace)
opt.Dialog = new(trace.Dialog)
opt.trace = t.(*trace.Trace)
opt.dialog = new(trace.Dialog)
}
}
}
@@ -57,20 +94,31 @@ func WithTrace(t Trace) Option {
// WithLogger 设置logger以便打印关键日志
func WithLogger(logger *zap.Logger) Option {
return func(opt *option) {
opt.Logger = logger
opt.logger = logger
}
}
// WithRetryTimes 如果请求失败最多重试N次
func WithRetryTimes(retryTimes int) Option {
// WithMock 设置 mock 数据
func WithMock(mock Mock) Option {
return func(opt *option) {
opt.RetryTimes = retryTimes
opt.mock = mock
}
}
// WithRetryDelay 在重试前,延迟等待一会
func WithRetryDelay(retryDelay time.Duration) Option {
// WithOnFailedAlarm 设置告警通知
func WithOnFailedAlarm(alarmTitle string, alarmObject AlarmObject, alarmVerify AlarmVerify) Option {
return func(opt *option) {
opt.RetryDelay = retryDelay
opt.alarmTitle = alarmTitle
opt.alarmObject = alarmObject
opt.alarmVerify = alarmVerify
}
}
// WithOnFailedRetry 设置失败重试
func WithOnFailedRetry(retryTimes int, retryDelay time.Duration, retryVerify RetryVerify) Option {
return func(opt *option) {
opt.retryTimes = retryTimes
opt.retryDelay = retryDelay
opt.retryVerify = retryVerify
}
}

44
pkg/httpclient/retry.go Normal file
View File

@@ -0,0 +1,44 @@
package httpclient
import (
"context"
"net/http"
"time"
)
const (
// DefaultRetryTimes 如果请求失败最多重试3次
DefaultRetryTimes = 3
// DefaultRetryDelay 在重试前延迟等待100毫秒
DefaultRetryDelay = time.Millisecond * 100
)
// Verify parse the body and verify that it is correct
type RetryVerify func(body []byte) (shouldRetry bool)
func shouldRetry(ctx context.Context, httpCode int) bool {
select {
case <-ctx.Done():
return false
default:
}
switch httpCode {
case
_StatusReadRespErr,
_StatusDoReqErr,
http.StatusRequestTimeout,
http.StatusLocked,
http.StatusTooEarly,
http.StatusTooManyRequests,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout:
return true
default:
return false
}
}

View File

@@ -35,28 +35,40 @@ var defaultClient = &http.Client{
func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option) ([]byte, int, error) {
ts := time.Now()
if mock := opt.mock; mock != nil {
if opt.dialog != nil {
opt.dialog.AppendResponse(&trace.Response{
HttpCode: http.StatusOK,
HttpCodeMsg: http.StatusText(http.StatusOK),
Body: string(mock()),
CostSeconds: time.Since(ts).Seconds(),
})
}
return mock(), http.StatusOK, nil
}
req, err := http.NewRequest(method, url, bytes.NewReader(payload))
if err != nil {
return nil, -1, errors.Wrapf(err, "new request [%s %s] err", method, url)
}
req = req.WithContext(ctx)
for key, value := range opt.Header {
req.Header.Set(key, value)
for key, value := range opt.header {
req.Header.Set(key, value[0])
}
resp, err := defaultClient.Do(req)
if err != nil {
err = errors.Wrapf(err, "do request [%s %s] err", method, url)
if opt.Dialog != nil {
opt.Dialog.AppendResponse(&trace.Response{
if opt.dialog != nil {
opt.dialog.AppendResponse(&trace.Response{
Body: err.Error(),
CostSeconds: time.Since(ts).Seconds(),
})
}
if opt.Logger != nil {
opt.Logger.Warn("doHTTP got err", zap.Error(err))
if opt.logger != nil {
opt.logger.Warn("doHTTP got err", zap.Error(err))
}
return nil, _StatusDoReqErr, err
}
@@ -65,22 +77,22 @@ func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
err = errors.Wrapf(err, "read resp body from [%s %s] err", method, url)
if opt.Dialog != nil {
opt.Dialog.AppendResponse(&trace.Response{
if opt.dialog != nil {
opt.dialog.AppendResponse(&trace.Response{
Body: err.Error(),
CostSeconds: time.Since(ts).Seconds(),
})
}
if opt.Logger != nil {
opt.Logger.Warn("doHTTP got err", zap.Error(err))
if opt.logger != nil {
opt.logger.Warn("doHTTP got err", zap.Error(err))
}
return nil, _StatusReadRespErr, err
}
defer func() {
if opt.Dialog != nil {
opt.Dialog.AppendResponse(&trace.Response{
if opt.dialog != nil {
opt.dialog.AppendResponse(&trace.Response{
Header: resp.Header,
HttpCode: resp.StatusCode,
HttpCodeMsg: resp.Status,
@@ -91,11 +103,7 @@ func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option
}()
if resp.StatusCode != http.StatusOK {
return nil, resp.StatusCode, newReplyErr(
resp.StatusCode,
body,
errors.Errorf("do [%s %s] return code: %d message: %s", method, url, resp.StatusCode, string(body)),
)
return body, resp.StatusCode, errors.Errorf("do [%s %s] return code: %d message: %s", method, url, resp.StatusCode, string(body))
}
return body, http.StatusOK, nil