日志记录
本节将介绍如何在 Go-Sail 中进行日志记录。
简介
日志记录几乎是每个系统都会执行的任务。原因很简单:日志可以帮助你追踪事件,从而便于定位和排查问题。
为了帮助你更好地洞察应用内部的运行状况,Go-Sail 提供了强大的日志服务,支持将日志信息记录到文件或通过导出器(exporter)输出。
配置
Go-Sail 的日志组件基于 uber/zap 日志库,并在服务启动时自动激活。你可以通过配置来指定其行为。
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
LoggerConf: logger.Conf{
Level: "warn",
Filename: "running.log",
MaxSize: 100,
MaxBackups: 10,
Compress: true,
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// user not exist
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// password not match
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User{})
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
}
)
func main() {
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
上面高亮的示例代码配置了 warn 级别,日志输出到当前目录下名为 running.log 的文件,并限制单个文件最大 100MB。Go-Sail 的日志组件支持日志轮转,无需担心日志文件体积过大或过多导致磁盘被填满。MaxBackups 表示最多保留 10 个最近的日志文件,且滚动的旧日志将被压缩,以进一步节省空间。
日志等级
日志等级决定哪些级别的日志会被记录,这一筛选是在配置阶段完成的,与具体调用什么日志方法无关。日志级别从低到高分为:
- debug
- info
- warn
- error
调用时可根据紧急和重要性优先选择。例如,普通信息可用 Info() 方法输出,当配置等级高于 info 时,代码里的此类日志不会被写入日志文件,从而实现全局管控。
import (
"github.com/keepchen/go-sail/v3/sail"
)
func main() {
sail.GetLogger().Info("记录一些日志...")
}
当设置的日志等级高于 info 时,以上代码的日志不会被写入文件,通过此机制实现统一的日志输出策略。
控制台输出(ConsoleOutput)
默认情况下,日志只输出到文件。但有时你可能希望开发调试时同时在终端看到日志,这时候只需要在配置中加上 ConsoleOutput 配置即可:
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
LoggerConf: logger.Conf{
Level: "warn",
Filename: "running.log",
MaxSize: 100,
MaxBackups: 10,
Compress: true,
ConsoleOutput: true,
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// user not exist
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// password not match
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User{})
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
}
)
func main() {
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
多模块(Modules)
模块配置的作用是将不同来源的日志分类记录到不同文件里,便于管理。比如可以将定时任务的日志单独写入名为 schedule 的文件。由于定时任务定期执行,日志重要但不多,分离日志便于后续查看。
在配置中声明需要的模块即可,使用时再调用对应模块:
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
LoggerConf: logger.Conf{
Level: "warn",
Filename: "running.log",
MaxSize: 100,
MaxBackups: 10,
Compress: true,
ConsoleOutput: true,
Modules: []string{"schedule"},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
c.ShouldBind(&loginRequest)
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// user not exist
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// password not match
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User{})
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("logging something...")
}
)
func main() {
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
如果你调用了一个没有配置的模块,日志会默认写到默认日志文件。
例如下面两行日志,都会被记录到同一个地方:
sail.GetLogger().Info("...")
sail.GetLogger("no-existent-module").Info("...")
日志导出器(Exporter)
通常情况下,Go-Sail 日志会输出到本地文件。这在开发环境并无问题,但在复杂的服务器或微服务环境下,本地存储不可靠且不可持久,日志通常需要转发到中央日志服务或 ELK 等日志中心。这时就可以使用日志导出器。
Go-Sail 的日志导出器会将日志同步到消息队列类中间件,后续由其它消费者消费和处理。目前支持以下中间件:
- Redis(单机)
- Redis(集群)
- Nats
- Kafka
注意,为保证数据不丢失,Redis 的导出器不是采用发布-订阅,而是用 List 结构执行 RPush,消费端应使用 LPop 获取。
声明日志导出器时,相关中间件如 Redis、NATS、Kafka 的连接信息需要分别单独配置。
这种设计是有意为之,为了核心服务的稳定性,初版就将日志导出配置与核心服务配置分离。若你没此类需求,也可以把两部分配置写成一样即可。
分布式链路追踪
用法
在 Web 应用程序中,记录和追踪整条链路(请求-处理-响应)很常见。
我们可能希望追踪关键事务或错误日志的全流程,便于排查与优化。这时就可用上分布式链路追踪。Go-Sail 启动时会自动启用链路追踪,你可以在路由处理函数中自由调用。
package main
import (
"fmt"
"time"
"go.uber.org/zap"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
LoggerConf: logger.Conf{
Level: "warn",
Filename: "running.log",
MaxSize: 100,
MaxBackups: 10,
Compress: true,
ConsoleOutput: true,
Modules: []string{"schedule"},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("bind request parameter failed", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// user not exist
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// password not match
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User{})
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("logging something...")
}
)
func main() {
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
组件透传
要启用链路日志,推荐从上下文中获取日志组件进行输出,也可以将日志组件往下层函数传递实现更统一的日志记录。
package main
import (
"fmt"
"time"
"go.uber.org/zap"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
LoggerConf: logger.Conf{
Level: "warn",
Filename: "running.log",
MaxSize: 100,
MaxBackups: 10,
Compress: true,
ConsoleOutput: true,
Modules: []string{"schedule"},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var (
loginRequest LoginRequest
loggerSvc = sail.LogTrace(c).GetLogger()
)
if err := c.ShouldBind(&loginRequest); err != nil {
loggerSvc.Warn("bind request parameter failed", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// user not exist
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// password not match
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
toDoSomething(loggerSvc)
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User{})
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("logging something...")
}
)
func main() {
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
微服务间链路追踪
在微服务环境下,多服务之间相互发起请求很常见。要想串联整个请求链路,需要传递请求唯一标识。如果你所有服务都用 Go-Sail,实现会自动处理。
以 Http 请求为例,只需在请求头加上 requestId 或 X-Request-Id 字段,值从当前请求上下文读取即可。
示例:
package main
import (
"fmt"
"time"
"go.uber.org/zap"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf = &config.Config{
DBConf: db.Conf{
Enable: true,
DriverName: "mysql",
Mysql: db.MysqlConf{
Read: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
Write: db.MysqlConfItem{
Host: "127.0.0.1",
Port: 3306,
Username: "root",
Password: "root",
Database: "go_sail",
},
},
Logger: db.Logger{
Level: "warn",
SlowThreshold: 100,
SkipCallerLookup: true,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
NowFunc: func() time.Time {
return time.Now().In(time.UTC)
},
},
LoggerConf: logger.Conf{
Level: "warn",
Filename: "running.log",
MaxSize: 100,
MaxBackups: 10,
Compress: true,
ConsoleOutput: true,
Modules: []string{"schedule"},
},
}
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("bind request parameter failed", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// user not exist
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// password not match
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
headers := map[string]string{
"X-Request-Id": sail.LogTrace(c).RequestID(),
}
sail.Utils().HttpClient().SendRequest("POST", "https://....", nil, headers)
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User{})
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("logging something...")
}
)
func main() {
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
目前 Go-Sail 只自动处理 HTTP 请求,如果你的服务使用其它协议(如 gRPC),需要自行集成,但原理一致。