upgrade
This commit is contained in:
42
README.md
42
README.md
@@ -1,4 +1,4 @@
|
||||
## go-gin-api
|
||||
## go-gin-api 
|
||||
|
||||
[](https://github.com/xinliangnote/go-gin-api/stargazers)
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
12
configs/prometheus/prometheus.yml
Normal file
12
configs/prometheus/prometheus.yml
Normal 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
3
go.mod
@@ -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
27
go.sum
@@ -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=
|
||||
|
||||
@@ -23,6 +23,7 @@ var (
|
||||
ErrUserCreate = errno.NewError(20102, "创建用户失败")
|
||||
ErrUserUpdate = errno.NewError(20103, "更新用户失败")
|
||||
ErrUserSearch = errno.NewError(20104, "查询用户失败")
|
||||
ErrUserHTTP = errno.NewError(20105, "调用他方接口失败")
|
||||
|
||||
// ...
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
24
internal/api/repository/third_party_request/alarm.go
Normal file
24
internal/api/repository/third_party_request/alarm.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package jsonparse
|
||||
|
||||
import "github.com/tidwall/gjson"
|
||||
|
||||
func Get(json, path string) interface{} {
|
||||
return gjson.Get(json, path).Value()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,执行时长,单位:秒
|
||||
- sql,SQL 语句
|
||||
- rows_affected,影响行数
|
||||
|
||||
|
||||
@@ -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
29
pkg/httpclient/alarm.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
44
pkg/httpclient/retry.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user