日志服务
在服务端开发中,日志系统必不可少的,能帮助我们更快的找到问题和运行记录,接下来就简单的做个日志文件系统。
日志是以文件的形式存放在项目的目录中,所以需要使用Go对文件操作,封装一下
pkg/file/file.go
package file
import (
"io/ioutil"
"mime/multipart" //它主要实现了 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
}
//检查文件权限
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
}
/*
调用文件,支持传入文件名称、指定的模式调用文件、文件权限,返回的文件的方法可以用于I/O。如果出现错误,则为*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 // 在写入时将数据追加到文件中
O_CREATE int = syscall.O_CREAT // 如果不存在,则创建一个新文件
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
}
这个文件把大部分文件操作的方法封装了。注释中也有讲解。而且以后再有对文件进行操作(比如用户上传/更改头像,上传文件)也都可以使用这些方法进行操作。
然后进行对log的日志文件进行操作
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() //返回与当前目录对应的根路径名
if err != nil {
return nil, fmt.Errorf("os.Getwd err: %v", err)
}
src := dir + "/" + filePath
perm := file.CheckPermission(src) //检查文件权限
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)
}
//调用文件,在写入时将数据追加到文件中 | 如果不存在,则创建一个新文件 | 以只写模式打开文件
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
为命名文件。日志文件夹保存的路径在是根据配置文件设置的,路径为runtime/logs/
下
最后对打印日志的格式进行封装
pkg/logging/log.go
package logging
import (
"fmt"
"log"
"os"
"path/filepath"
"runtime"
)
type Level int
var (
F * os.File //文件
DefaultPrefix = "" //默认的前缀
DefaultCallDepth = 2 //调用深度
logger *log.Logger //打印
logPrefix = "" //打印前缀
levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"} //调试,信息,警告,错误,崩溃
)
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创建一个新的日志记录器。out定义要写入日志数据的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)
}
//设置前缀
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个级别,调试,信息,警告,错误,崩溃。可以根据不同环境下的信息进行调用不同的级别。
在main.go
文件中初始化
...
setting.SetUp() //初始化配置文件
logging.SetUp() //设置日志文件
...
路由文件
用Gin框架启动服务没问题后,进行路由文件的编写。
创建routers/routers.go
文件,在这个文件中创建初始化路由的方法
routers/routers.go
package routers
import (
"api/pkg/e"
"api/pkg/setting"
"github.com/gin-gonic/gin"
)
/*
初始化路由
*/
func InitRouter() *gin.Engine {
r := gin.New() //创建gin框架路由实例
r.Use(gin.Logger()) //使用gin框架中的打印中间件
r.Use(gin.Recovery()) //使用gin框架中的恢复中间件,可以从任何恐慌中恢复,如果有,则写入500
gin.SetMode(setting.ServerSetting.RunMode) //设置运行模式,debug或release,如果放在gin.New或者gin.Default之后,还是会打印一些信息的。放之前则不会
apiv1 := r.Group("/api/v1") //路由分组,apiv1代表v1版本的路由组
{
apiv1.GET("version",v1.GetAppVersionTest) //app版本升级
}
return r
}
创建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":"返回数据成功",
})
}
同样的,也必须把main方法中的初始化路由修改下
main.go
...
func main() {
log.Println("Hello, api 正在启动中...")
setting.SetUp() //初始化配置文件
router := routers.InitRouter() //初始化路由
s := &http.Server{
Addr:fmt.Sprintf(":%d", setting.ServerSetting.HttpPort), //设置端口号
Handler:router, //http句柄,实质为ServeHTTP,用于处理程序响应HTTP请求
ReadTimeout:setting.ServerSetting.ReadTimeout, //允许读取的最大时间
WriteTimeout:setting.ServerSetting.WriteTimeout, //允许写入的最大时间
MaxHeaderBytes: 1 << 20, //请求头的最大字节数
}
/*
使用 http.Server - Shutdown() 优雅的关闭http服务
*/
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("程序服务关闭退出")
}
可以看到对路由分组以后,现在接口的url是http://127.0.0.1:9999/api/v1/version
重新进行go run
后,浏览器查看是否返回数据
以后的路由代码都会在routers/v1
目录下编写,比如app_version.go
文件就是有关app升级相关的代码,这里就把路由服务分解开来了。