From 2466dfcbfca39c5a4d11b1c9e4737585c0f25095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B0=E4=BA=AE?= Date: Sat, 16 Jan 2021 14:45:42 +0800 Subject: [PATCH] upgrade --- README.md | 42 ++- cmd/api/main.go | 2 +- .../{dev.configs.toml => dev_configs.toml} | 0 .../{pro.configs.toml => pro_configs.toml} | 0 configs/prometheus/prometheus.yml | 12 + go.mod | 3 +- go.sum | 27 -- internal/api/code/code.go | 1 + internal/api/controller/demo/demo.go | 48 ++-- internal/api/repository/db_repo/plugin.go | 76 +++--- .../db_repo/user_demo_repo/user_demo.go | 5 +- .../repository/third_party_request/alarm.go | 24 ++ .../go_gin_api_repo/go_gin_api.go | 109 ++++++++ .../go_gin_api_repo/go_gin_api_mock.go | 25 ++ .../go_gin_api_repo/go_gin_api_test.go | 80 ++++++ internal/api/router/router.go | 8 +- internal/pkg/core/core.go | 13 +- internal/pkg/jsonparse/jsonparse.go | 7 - internal/pkg/metrics/metrics.go | 45 +++- internal/pkg/notify/notify.go | 4 +- internal/pkg/notify/{mail => }/template.go | 4 +- .../notify/{mail => }/templates/panic-mail.go | 0 internal/pkg/trace/README.md | 6 +- internal/pkg/trace/sql.go | 10 +- .../{go-gin-api.log => go-gin-api-access.log} | 0 pkg/httpclient/alarm.go | 29 ++ pkg/httpclient/client.go | 249 ++++++++++++------ pkg/httpclient/error.go | 46 ---- pkg/httpclient/option.go | 92 +++++-- pkg/httpclient/retry.go | 44 ++++ pkg/httpclient/util.go | 42 +-- {internal/pkg/notify => pkg}/mail/mail.go | 0 .../pkg/notify => pkg}/mail/mail_test.go | 0 33 files changed, 753 insertions(+), 300 deletions(-) rename configs/{dev.configs.toml => dev_configs.toml} (100%) rename configs/{pro.configs.toml => pro_configs.toml} (100%) create mode 100644 configs/prometheus/prometheus.yml create mode 100644 internal/api/repository/third_party_request/alarm.go create mode 100644 internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api.go create mode 100644 internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_mock.go create mode 100644 internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_test.go delete mode 100644 internal/pkg/jsonparse/jsonparse.go rename internal/pkg/notify/{mail => }/template.go (91%) rename internal/pkg/notify/{mail => }/templates/panic-mail.go (100%) rename logs/{go-gin-api.log => go-gin-api-access.log} (100%) create mode 100644 pkg/httpclient/alarm.go delete mode 100644 pkg/httpclient/error.go create mode 100644 pkg/httpclient/retry.go rename {internal/pkg/notify => pkg}/mail/mail.go (100%) rename {internal/pkg/notify => pkg}/mail/mail_test.go (100%) diff --git a/README.md b/README.md index 5ba7c88..9403396 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/api/main.go b/cmd/api/main.go index 717a374..9895056 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -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) diff --git a/configs/dev.configs.toml b/configs/dev_configs.toml similarity index 100% rename from configs/dev.configs.toml rename to configs/dev_configs.toml diff --git a/configs/pro.configs.toml b/configs/pro_configs.toml similarity index 100% rename from configs/pro.configs.toml rename to configs/pro_configs.toml diff --git a/configs/prometheus/prometheus.yml b/configs/prometheus/prometheus.yml new file mode 100644 index 0000000..f518e0e --- /dev/null +++ b/configs/prometheus/prometheus.yml @@ -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" diff --git a/go.mod b/go.mod index 19d56c7..d8cf85e 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 033817e..12765ed 100644 --- a/go.sum +++ b/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= diff --git a/internal/api/code/code.go b/internal/api/code/code.go index 654f86a..2e58e37 100644 --- a/internal/api/code/code.go +++ b/internal/api/code/code.go @@ -23,6 +23,7 @@ var ( ErrUserCreate = errno.NewError(20102, "创建用户失败") ErrUserUpdate = errno.NewError(20103, "更新用户失败") ErrUserSearch = errno.NewError(20104, "查询用户失败") + ErrUserHTTP = errno.NewError(20105, "调用他方接口失败") // ... ) diff --git a/internal/api/controller/demo/demo.go b/internal/api/controller/demo/demo.go index 02c134e..027f6bb 100644 --- a/internal/api/controller/demo/demo.go +++ b/internal/api/controller/demo/demo.go @@ -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)) } diff --git a/internal/api/repository/db_repo/plugin.go b/internal/api/repository/db_repo/plugin.go index b415466..eb36fe7 100644 --- a/internal/api/repository/db_repo/plugin.go +++ b/internal/api/repository/db_repo/plugin.go @@ -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 +} diff --git a/internal/api/repository/db_repo/user_demo_repo/user_demo.go b/internal/api/repository/db_repo/user_demo_repo/user_demo.go index cffc2e3..a57fe89 100644 --- a/internal/api/repository/db_repo/user_demo_repo/user_demo.go +++ b/internal/api/repository/db_repo/user_demo_repo/user_demo.go @@ -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 diff --git a/internal/api/repository/third_party_request/alarm.go b/internal/api/repository/third_party_request/alarm.go new file mode 100644 index 0000000..adf6993 --- /dev/null +++ b/internal/api/repository/third_party_request/alarm.go @@ -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) +} diff --git a/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api.go b/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api.go new file mode 100644 index 0000000..d24848c --- /dev/null +++ b/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api.go @@ -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 +} diff --git a/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_mock.go b/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_mock.go new file mode 100644 index 0000000..fdfa057 --- /dev/null +++ b/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_mock.go @@ -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 +} diff --git a/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_test.go b/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_test.go new file mode 100644 index 0000000..3cf45b4 --- /dev/null +++ b/internal/api/repository/third_party_request/go_gin_api_repo/go_gin_api_test.go @@ -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 +} diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 317a533..5afa24f 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -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 diff --git a/internal/pkg/core/core.go b/internal/pkg/core/core.go index 5430e90..9d5fe7d 100644 --- a/internal/pkg/core/core.go +++ b/internal/pkg/core/core.go @@ -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() diff --git a/internal/pkg/jsonparse/jsonparse.go b/internal/pkg/jsonparse/jsonparse.go deleted file mode 100644 index 993abae..0000000 --- a/internal/pkg/jsonparse/jsonparse.go +++ /dev/null @@ -1,7 +0,0 @@ -package jsonparse - -import "github.com/tidwall/gjson" - -func Get(json, path string) interface{} { - return gjson.Get(json, path).Value() -} diff --git a/internal/pkg/metrics/metrics.go b/internal/pkg/metrics/metrics.go index 3721716..94b9c2a 100644 --- a/internal/pkg/metrics/metrics.go +++ b/internal/pkg/metrics/metrics.go @@ -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) } diff --git a/internal/pkg/notify/notify.go b/internal/pkg/notify/notify.go index 807ecf8..0222d65 100644 --- a/internal/pkg/notify/notify.go +++ b/internal/pkg/notify/notify.go @@ -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 diff --git a/internal/pkg/notify/mail/template.go b/internal/pkg/notify/template.go similarity index 91% rename from internal/pkg/notify/mail/template.go rename to internal/pkg/notify/template.go index fdcd861..283f126 100644 --- a/internal/pkg/notify/mail/template.go +++ b/internal/pkg/notify/template.go @@ -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 diff --git a/internal/pkg/notify/mail/templates/panic-mail.go b/internal/pkg/notify/templates/panic-mail.go similarity index 100% rename from internal/pkg/notify/mail/templates/panic-mail.go rename to internal/pkg/notify/templates/panic-mail.go diff --git a/internal/pkg/trace/README.md b/internal/pkg/trace/README.md index ca176cb..167349d 100644 --- a/internal/pkg/trace/README.md +++ b/internal/pkg/trace/README.md @@ -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,影响行数 diff --git a/internal/pkg/trace/sql.go b/internal/pkg/trace/sql.go index f2d051b..476843d 100644 --- a/internal/pkg/trace/sql.go +++ b/internal/pkg/trace/sql.go @@ -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"` // 影响行数 } diff --git a/logs/go-gin-api.log b/logs/go-gin-api-access.log similarity index 100% rename from logs/go-gin-api.log rename to logs/go-gin-api-access.log diff --git a/pkg/httpclient/alarm.go b/pkg/httpclient/alarm.go new file mode 100644 index 0000000..8a08eb7 --- /dev/null +++ b/pkg/httpclient/alarm.go @@ -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("
") + } + + if err := alarmObject.Send(title, buf.String()); err != nil && logger != nil { + logger.Error("calls failed alarm err", zap.Error(err)) + } +} diff --git a/pkg/httpclient/client.go b/pkg/httpclient/client.go index a6eb491..0530828 100644 --- a/pkg/httpclient/client.go +++ b/pkg/httpclient/client.go @@ -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 } diff --git a/pkg/httpclient/error.go b/pkg/httpclient/error.go deleted file mode 100644 index b6b23c8..0000000 --- a/pkg/httpclient/error.go +++ /dev/null @@ -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 -} diff --git a/pkg/httpclient/option.go b/pkg/httpclient/option.go index 74f1c54..a9ea455 100644 --- a/pkg/httpclient/option.go +++ b/pkg/httpclient/option.go @@ -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 } } diff --git a/pkg/httpclient/retry.go b/pkg/httpclient/retry.go new file mode 100644 index 0000000..4883f00 --- /dev/null +++ b/pkg/httpclient/retry.go @@ -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 + } +} diff --git a/pkg/httpclient/util.go b/pkg/httpclient/util.go index 2a94db9..806b1d0 100644 --- a/pkg/httpclient/util.go +++ b/pkg/httpclient/util.go @@ -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 diff --git a/internal/pkg/notify/mail/mail.go b/pkg/mail/mail.go similarity index 100% rename from internal/pkg/notify/mail/mail.go rename to pkg/mail/mail.go diff --git a/internal/pkg/notify/mail/mail_test.go b/pkg/mail/mail_test.go similarity index 100% rename from internal/pkg/notify/mail/mail_test.go rename to pkg/mail/mail_test.go