日志服務(wù)
在服務(wù)端開發(fā)中,日志系統(tǒng)必不可少的瞒滴,能幫助我們更快的找到問題和運行記錄曲梗,接下來就簡單的做個日志文件系統(tǒng)。
日志是以文件的形式存放在項目的目錄中妓忍,所以需要使用Go對文件操作,封裝一下
pkg/file/file.go
package file
import (
"io/ioutil"
"mime/multipart" //它主要實現(xiàn)了 MIME 的 multipart 解析虏两,主要適用于 HTTP 和常見瀏覽器生成的 multipart 主體
"os"
"path"
)
//獲取文件大小
func GetSize(f multipart.File)(int , error) {
content,err := ioutil.ReadAll(f)
return len(content),err
}
//獲取文件后綴
func GetExt(filename string) string {
return path.Ext(filename)
}
//檢查文件是否存在
/*
如果返回的錯誤為nil,說明文件或文件夾存在
如果返回的錯誤類型使用os.IsNotExist()判斷為true,說明文件或文件夾不存在
如果返回的錯誤為其它類型,則不確定是否在存在
*/
func CheckExist(src string)bool {
_, err := os.Stat(src)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
//檢查文件權(quán)限
func CheckPermission(src string)bool {
_,err := os.Stat(src)
return os.IsPermission(err)
}
//新建文件夾
func MKDir(src string)error {
err := os.MkdirAll(src,os.ModePerm)
return err
}
//如果不存在則新建文件夾
func IsNotExistMkDir(src string)error {
if exist := CheckExist(src); exist == false {
if err := MKDir(src);err !=nil {
return err
}
}
return nil
}
/*
調(diào)用文件,支持傳入文件名稱世剖、指定的模式調(diào)用文件碘举、文件權(quán)限,返回的文件的方法可以用于I/O搁廓。如果出現(xiàn)錯誤引颈,則為*PathError
const (
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // 以只讀模式打開文件
O_WRONLY int = syscall.O_WRONLY // 以只寫模式打開文件
O_RDWR int = syscall.O_RDWR // 以讀寫模式打開文件
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // 在寫入時將數(shù)據(jù)追加到文件中
O_CREATE int = syscall.O_CREAT // 如果不存在,則創(chuàng)建一個新文件
O_EXCL int = syscall.O_EXCL // 使用O_CREATE時境蜕,文件必須不存在
O_SYNC int = syscall.O_SYNC // 同步IO
O_TRUNC int = syscall.O_TRUNC // 如果可以蝙场,打開時
)
*/
func Open(name string,flag int,perm os.FileMode)(*os.File,error) {
f,err := os.OpenFile(name,flag,perm)
if err != nil {
return nil,err
}
return f,err
}
這個文件把大部分文件操作的方法封裝了。注釋中也有講解粱年。而且以后再有對文件進(jìn)行操作(比如用戶上傳/更改頭像售滤,上傳文件)也都可以使用這些方法進(jìn)行操作。
然后進(jìn)行對log的日志文件進(jìn)行操作
pkg/logging/file.go
package logging
import (
"api/pkg/file"
"api/pkg/setting"
"fmt"
"os"
"time"
)
//獲取日志文件路徑
func getLogFilePath() string {
return fmt.Sprintf("%s%s", setting.AppSetting.RuntimeRootPath, setting.AppSetting.LogSavePath)
}
//獲取日志文件的名稱
func getLogFileName() string {
return fmt.Sprintf("%s%s.%s",
setting.AppSetting.LogSaveName,
time.Now().Format(setting.AppSetting.TimeFormat),
setting.AppSetting.LogFileExt,
)
}
//打開日志文件
func openLogFile(fileName, filePath string) (*os.File, error) {
dir, err := os.Getwd() //返回與當(dāng)前目錄對應(yīng)的根路徑名
if err != nil {
return nil, fmt.Errorf("os.Getwd err: %v", err)
}
src := dir + "/" + filePath
perm := file.CheckPermission(src) //檢查文件權(quán)限
if perm == true {
return nil, fmt.Errorf("file.CheckPermission Permission denied src: %s", src)
}
err = file.IsNotExistMkDir(src) //如果不存在則新建文件夾
if err != nil {
return nil, fmt.Errorf("file.IsNotExistMkDir src: %s, err: %v", src, err)
}
//調(diào)用文件台诗,在寫入時將數(shù)據(jù)追加到文件中 | 如果不存在完箩,則創(chuàng)建一個新文件 | 以只寫模式打開文件
f, err := file.Open(src + fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("Fail to OpenFile :%v", err)
}
return f, nil
}
日志的文件操作,將會以log+日期.log
為命名文件拉队。日志文件夾保存的路徑在是根據(jù)配置文件設(shè)置的弊知,路徑為runtime/logs/
下
最后對打印日志的格式進(jìn)行封裝
pkg/logging/log.go
package logging
import (
"fmt"
"log"
"os"
"path/filepath"
"runtime"
)
type Level int
var (
F * os.File //文件
DefaultPrefix = "" //默認(rèn)的前綴
DefaultCallDepth = 2 //調(diào)用深度
logger *log.Logger //打印
logPrefix = "" //打印前綴
levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"} //調(diào)試,信息粱快,警告秩彤,錯誤叔扼,崩潰
)
const (
DEBUG Level = iota
INFO
WARNING
ERROR
FATAL
)
func SetUp() {
var err error
filePath := getLogFilePath() //獲取日志文件路徑
fileName := getLogFileName() //獲取日志文件名稱
F, err = openLogFile(fileName, filePath) //打開日志文件
if err != nil {
log.Fatalln(err)
}
/*
log.New創(chuàng)建一個新的日志記錄器。out定義要寫入日志數(shù)據(jù)的IO句柄漫雷。prefix定義每個生成的日志行的開頭瓜富。flag定義了日志記錄屬性
log.LstdFlags:日志記錄的格式屬性之一,其余的選項如下
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
LstdFlags = Ldate | Ltime // initial values for the standard logger
)
*/
logger = log.New(F, DefaultPrefix, log.LstdFlags)
}
//設(shè)置前綴
func setPrefix(level Level) {
_,file,line,ok := runtime.Caller(DefaultCallDepth)
if ok {
logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
}else {
logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
}
logger.SetPrefix(logPrefix)
}
func Debug(v ...interface{}) {
setPrefix(DEBUG)
logger.Println(v)
}
func Info(v ...interface{}) {
setPrefix(INFO)
logger.Println(v)
}
func Warn(v ...interface{}) {
setPrefix(WARNING)
logger.Println(v)
}
func Error(v ...interface{}) {
setPrefix(ERROR)
logger.Println(v)
}
func Fatal(v ...interface{}) {
setPrefix(FATAL)
logger.Fatalln(v)
}
日志打印有5個級別降盹,調(diào)試与柑,信息,警告蓄坏,錯誤价捧,崩潰〗1瑁可以根據(jù)不同環(huán)境下的信息進(jìn)行調(diào)用不同的級別。
在main.go
文件中初始化
...
setting.SetUp() //初始化配置文件
logging.SetUp() //設(shè)置日志文件
...
路由文件
用Gin框架啟動服務(wù)沒問題后渠欺,進(jìn)行路由文件的編寫妹蔽。
創(chuàng)建routers/routers.go
文件,在這個文件中創(chuàng)建初始化路由的方法
routers/routers.go
package routers
import (
"api/pkg/e"
"api/pkg/setting"
"github.com/gin-gonic/gin"
)
/*
初始化路由
*/
func InitRouter() *gin.Engine {
r := gin.New() //創(chuàng)建gin框架路由實例
r.Use(gin.Logger()) //使用gin框架中的打印中間件
r.Use(gin.Recovery()) //使用gin框架中的恢復(fù)中間件挠将,可以從任何恐慌中恢復(fù)胳岂,如果有,則寫入500
gin.SetMode(setting.ServerSetting.RunMode) //設(shè)置運行模式舔稀,debug或release,如果放在gin.New或者gin.Default之后乳丰,還是會打印一些信息的。放之前則不會
apiv1 := r.Group("/api/v1") //路由分組内贮,apiv1代表v1版本的路由組
{
apiv1.GET("version",v1.GetAppVersionTest) //app版本升級
}
return r
}
創(chuàng)建routers/v1/app_version.go
package v1
import (
"api/pkg/e"
"github.com/gin-gonic/gin"
)
//app更新接口
func GetAppVersionTest(c *gin.Context) {
c.JSON(e.SUCCESS,gin.H{
"Code":e.SUCCESS,
"Msg":e.GetMsg(e.SUCCESS),
"Data":"返回數(shù)據(jù)成功",
})
}
同樣的产园,也必須把main方法中的初始化路由修改下
main.go
...
func main() {
log.Println("Hello, api 正在啟動中...")
setting.SetUp() //初始化配置文件
router := routers.InitRouter() //初始化路由
s := &http.Server{
Addr:fmt.Sprintf(":%d", setting.ServerSetting.HttpPort), //設(shè)置端口號
Handler:router, //http句柄,實質(zhì)為ServeHTTP夜郁,用于處理程序響應(yīng)HTTP請求
ReadTimeout:setting.ServerSetting.ReadTimeout, //允許讀取的最大時間
WriteTimeout:setting.ServerSetting.WriteTimeout, //允許寫入的最大時間
MaxHeaderBytes: 1 << 20, //請求頭的最大字節(jié)數(shù)
}
/*
使用 http.Server - Shutdown() 優(yōu)雅的關(guān)閉http服務(wù)
*/
go func() {
if err := s.ListenAndServe(); err != nil{
log.Printf("Listen: %s\n", err)
}
}()
quit := make(chan os.Signal)
signal.Notify(quit,os.Interrupt)
<- quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("程序服務(wù)關(guān)閉退出")
}
可以看到對路由分組以后什燕,現(xiàn)在接口的url是http://127.0.0.1:9999/api/v1/version
重新進(jìn)行go run
后,瀏覽器查看是否返回數(shù)據(jù)
以后的路由代碼都會在routers/v1
目錄下編寫竞端,比如app_version.go
文件就是有關(guān)app升級相關(guān)的代碼屎即,這里就把路由服務(wù)分解開來了。