安全法要求日志保存180天,很多网络设备基本都没法达到这个要求,但是都可以发送syslog,为了保存各个设备的日志文件,就手撸了一个日志服务器。
1、本日志服务器用于接收RFC3164格式的日志,分设备放置于不同文件夹,分日期进行切片存放,一个切片日志6M。有其他需要可以自己调整重新编译。
2、日志服务器默认使用udp 514端口,请在防火墙上设置允许通过。
3、日志服务器会在每天凌晨12点打包上一日的日志,方便进行存储。Windows下可能存在权限问题,右键属性设置一下即可。
PS:
1、该日志服务器解析RFC3164格式的日志,如果需要支持其他格式的日志,可以在源码中调整,可以支持RFC5424和RFC6587。
2、golang写入性能还不错,所以未添加写入缓存,如有需要,可以自行添加。
源码给出 大家可以自己编译。
main.go
[Golang] 纯文本查看 复制代码
package main
import (
"fmt"
"io"
"os"
"runtime"
"strconv"
"strings"
"time"
"gopkg.in/mcuadros/go-syslog.v2"
)
var fg string
var hg string
var fdate string
var loghostf map[string]int //键值对,记录主机和日志切片数据,重新启动程序会自动刷新
var logfilemax = 6 * 1024 * 1024 //6M一个分片
var dt string
var ot string //上一日
var Logbase = "logs"
func main() {
systype := runtime.GOOS
if systype == "windows" {
fg = "\\"
hg = "\r\n"
} else {
fg = "/"
hg = "\n"
}
//dt = "2021-09-13" //测试数据,正式使用请删除
dt = time.Now().Format("2006-01-02")
loghostf = make(map[string]int) //计数器
_, err := os.Stat(Logbase)
if os.IsNotExist(err) {
os.Mkdir(Logbase, os.ModePerm) //建立日志存放文件夹
}
channel := make(syslog.LogPartsChannel)
handler := syslog.NewChannelHandler(channel)
server := syslog.NewServer() //建立syslog服务器
server.SetFormat(syslog.RFC3164) //日志格式
server.SetHandler(handler)
server.ListenUDP("0.0.0.0:514")
server.Boot()
go func(channel syslog.LogPartsChannel) {
for logParts := range channel {
nt := time.Now().Format("2006-01-02") //日期发生变化时更新日期常数
if strings.Compare(dt, nt) != 0 {
ot = dt
dt = nt
if loghostf != nil { //如果日期更新就初始化计数器
for k := range loghostf {
delete(loghostf, k)
}
}
go Dozipfordel(ot)
}
doinfile(logParts["hostname"].(string), logParts["content"].(string))
fmt.Println(logParts)
}
}(channel)
server.Wait()
}
func doinfile(fhost string, fmsg string) { //写入日志文件
fpath := logfilestat(fhost)
f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, os.ModePerm)
_, err = io.WriteString(f, fmsg+hg)
if err != nil {
fmt.Println("写入文件错误,请检查logs保存路径!")
}
}
func logfilestat(fhostf string) string { //判断日志文件状态并进行切片
fnum, fa := loghostf[fhostf]
if fa == false { //判断键值是否存在
_, err1 := os.Stat("logs" + fg + fhostf)
if os.IsNotExist(err1) {
os.MkdirAll("logs"+fg+fhostf, os.ModePerm) //新的hostname就新建对应的文件夹
}
loghostf[fhostf] = 0 //第一次初始化写入键值
}
fp := "logs" + fg + fhostf + fg + dt + "-" + strconv.Itoa(fnum) + ".log"
ff, err2 := os.Stat(fp) //获取文件状态
if err2 == nil && fnum == 0 && ff.Size() > int64(logfilemax) { //重新启动时找到正确的切片值
for fexist(fp) {
fnum = fnum + 1
fp = "logs" + fg + fhostf + fg + dt + "-" + strconv.Itoa(fnum) + ".log"
}
loghostf[fhostf] = fnum
//fp = "logs" + fg + fhostf + fg + dt + "-" + strconv.Itoa(fnum) + ".log"
} else {
if err2 == nil && ff.Size() > int64(logfilemax) { //超过分片大小就新生成一个子文件
fnum = fnum + 1
loghostf[fhostf] = fnum
fp = "logs" + fg + fhostf + fg + dt + "-" + strconv.Itoa(fnum) + ".log"
}
}
return fp
}
func fexist(fp string) bool { //判断文件是否存在
ff, err := os.Stat(fp)
if os.IsNotExist(err) || ff.Size() < int64(logfilemax) { //序列号不存在或该序列号对应的文件小于分割文件大小
return false
} else {
return true
}
}
zip.go
[Asm] 纯文本查看 复制代码
package main
import (
"archive/zip"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
/**
@files:需要压缩的文件
@compreFile:压缩之后的文件
*/
func Compress_zip(files []*os.File, compreFile *os.File) (err error) {
zw := zip.NewWriter(compreFile)
defer zw.Close()
for _, file := range files {
err := compress_zip(file, zw)
if err != nil {
return err
}
file.Close()
}
return nil
}
/**
功能:压缩文件
@file:压缩文件
@prefix:压缩文件内部的路径
@tw:写入压缩文件的流
*/
func compress_zip(file *os.File, zw *zip.Writer) error {
info, err := file.Stat()
if err != nil {
//logs.Error("压缩文件失败:", err.Error())
return err
}
// 获取压缩头信息
head, err := zip.FileInfoHeader(info)
if err != nil {
//logs.Error("压缩文件失败:", err.Error())
return err
}
// 指定文件压缩方式 默认为 Store 方式 该方式不压缩文件 只是转换为zip保存
head.Method = zip.Deflate
fw, err := zw.CreateHeader(head)
if err != nil {
//logs.Error("压缩文件失败:", err.Error())
return err
}
// 写入文件到压缩包中
_, err = io.Copy(fw, file)
file.Close()
if err != nil {
//logs.Error("压缩文件失败:", err.Error())
return err
}
return nil
}
/*
获取需要压缩的子文件夹列表
*/
func getDirList(dirpath string) ([]string, error) {
var dir_list []string
dirinfo, err := ioutil.ReadDir(dirpath)
if err == nil {
for _, dinfo := range dirinfo {
if dinfo.IsDir() {
dir_list = append(dir_list, dinfo.Name()) //写入文件夹列表
}
}
}
return dir_list, err
}
/*
对上一日的日志文件进行压缩后删除文件
*/
func Dozipfordel(ot string) { //上一日日期,进行日志文件压缩
dps, err := getDirList(Logbase)
if err == nil {
for _, dpt := range dps {
var zfiles []*os.File
var filesname []string
err = filepath.Walk(filepath.Join(Logbase, dpt), //便利log子目录下的所有文件,找到对应日期的文件
func(path string, f os.FileInfo, err error) error {
if f == nil {
return err
}
if strings.Contains(f.Name(), ot) {
fn := filepath.Join(Logbase, dpt, f.Name()) //获取文件名,添加到文件名队列,用来后面的删除,同时在打开文件句柄
filesname = append(filesname, fn)
of, _ := os.Open(fn)
//defer of.Close()
zfiles = append(zfiles, of)
}
return nil
})
if len(zfiles) != 0 { //如果文件列表中
nf, err := os.OpenFile(filepath.Join(Logbase, dpt, ot)+".zip", os.O_WRONLY|os.O_CREATE, os.ModePerm)
//defer nf.Close()
if err == nil {
if Compress_zip(zfiles, nf) == nil { //压缩成功后删除源文件
for _, fi := range filesname {
os.Remove(fi)
}
}
}
}
}
}
}
然后再放一个Windows下的编译好的文件,360可能有误报,自己判断。