Go 로그의 json 파일 내보내기

76482 단어 Gologtech

개시하다


광활한 인터넷 바다에 작은 방해 수색을 추가하다.

모티프


개인 개발 프로젝트의 운용에서 일지를 어떻게 보관하고 관리할 것인가를 고려하기 시작했다.
sentry에 의거하여zeroolog와logrus 등 좋은 포장을 사용해도 괜찮지만, 공부를 하면 어떻게든 고의 표준 포장으로 자제할 수 있다고 생각합니다.
goo의 장점은 역시 표준 포장이 딱 좋다.

파일에 로그 쓰기


나중에 읽을 수 있도록 서류 보관을 고려했다.
이것은 로그 포장으로 매우 간단합니다.
logger.go
package domain

import (
	".../configs"
	".../tools"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"runtime"
	"time"
)


const (
	/******************
	    probrem
	******************/
	// urgency (at once)
	LogAlert = "ALT"
	// urgency (at many times)
	LogCritical = "CRT"
	// need fix without urgency (at many times)
	LogWarn = "WAN"
	/******************
	   no probrem
	******************/
	LogInfo   = "INF"
	/******************
	   no probrem
	******************/
	LogDebug = "DBG"
)

// log出力汎用関数
func ErrorLogger(err error, mode string) {
    if IsProduction() {
        today := time.Now().Format("20060102")
        lFile, _ := os.OpenFile(fmt.Sprintf("%s/log_%s.log", configs.ErrorLogDirectory, today), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
        defer lFile.Close()

		// get called place
		_, file, line, _ := runtime.Caller(1)

		log.SetOutput(lFile) // 出力先の変更
		log.SetFlags(log.LstdFlags) // 不要、フラグを付け足すときにはここにパイプで追加

		// set prefix
		var level string
		switch mode {
		case "":
			level = LogInfo
		default:
			level = mode
		}
		log.SetPrefix(level+" ")

		log.Println(fmt.Sprintf("%s:%d: %v", file, line, err))
    } else {
        log.Println(err)
    }
}
제가 설명해 드릴게요.
runtime를 사용하여 호출 원본의 위치를 찾습니다.
_, file, line, _ := runtime.Caller(1)
SetOutput 방법으로 로그의 출력 위치를 변경할 수 있습니다.
log.SetOutput(lFile) // 出力先の変更
Laavel의 Log fathod와 같은 형식으로 로그 레벨을 접두사 문자로 제공합니다.
// set prefix
var level string
switch mode {
case "":
	level = LogInfo
default:
	level = mode
}
log.SetPrefix(level)
이렇게 하면 인쇄 파일이 이런 느낌으로 출력된다.
WAN 2021/07/28 08:05:56 /<秘密>/internal/pkg/interfaces/database/audience_repository.go:121: sql: Rows are closed
WAN 2021/07/28 08:05:58 /<秘密>/internal/pkg/interfaces/database/audience_repository.go:121: sql: Rows are closed

json 파일에 로그 쓰기


검색성을 높이고 차원화 가능성을 결정하는 json.

파일 출력


그때는 먼저 json 파일의 시작에 공을 들여야 하기 때문에 공유합니다.
json 파일을 출력하는 방법으로 두 가지를 고려했습니다.
  • 전체 파일 덮어쓰기
  • 필요한 부분을 파일에 추가
  • 나는 1의 방법이 좀 거칠다고 생각했는데 기준의 결과는 2의 방법이 조금 낫다고 생각해서 결국 2를 채택했다.
    1의 방법과 시험과 결과를 마지막으로 보충하여 기록한다.
    2의 방법(추기법)
    logger.go
    func writeJsonFile(fileName string, object interface{}) {
    	file, _ := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0600)
    	defer file.Close()
    	fi, _ := file.Stat()
    	leng := fi.Size()
    
    	json_, _ := json.Marshal(object)
    
    	if leng == 0 {
    		file.Write([]byte(fmt.Sprintf(`[%s]`, json_)))
    	} else {
    		file.WriteAt([]byte(fmt.Sprintf(`,%s]`, json_)), leng-1)
    	}
    }
    
    우선 쓰고 싶은 것을 json화해라.
    다음에 파일 길이를 가져옵니다.
    새 파일인 경우 직접 내보내기
    그렇지 않으면 마지막 문자],<追加分>] 문자열로 덮어씁니다.

    오류 로그를 json으로 설정하여 파일에 쓰기


    아까 writeJson () 을 사용하면 이렇게 쓸 수 있습니다.
    전문이 단숨에 완성되다.
    logger.go
    package domain
    
    import (
    	"animar/v1/configs"
    	"animar/v1/internal/pkg/tools/tools"
    	"encoding/json"
    	"fmt"
    	"net/http"
    	"os"
    	"runtime"
    	"time"
    )
    
    const (
    	/******************
    	    probrem
    	******************/
    	// urgency (at once)
    	LogAlert = "ALT"
    	// urgency (at many times)
    	LogCritical = "CRT"
    	// need fix without urgency
    	LogWarn = "WAN"
    	/******************
    	   no probrem
    	******************/
    	LogInfo = "INF"
    	/******************
    	   no probrem
    	******************/
    	LogDebug = "DBG"
    )
    
    // Logの基底構造体
    type Log struct {
    	Kind  string    `json:"kind"`
    	Time  time.Time `json:"time"`
    	Level string    `json:"level"`
    }
    
    // Error用ログの構造体
    type LogE struct {
    	Log
    	Content string `json:"content"` // エラー内容
    	Place   string `json:"place"` // エラー発生場所
    }
    
    // Access用ログの構造体
    type LogA struct {
    	Log
    	Address string `json:"address"` // IPアドレス
    	Method  string `json:"method"` // request method
    	Path    string `json:"path"` // path
    }
    
    func NewAccessLog() *LogA {
    	// startのlogをここで作る
    	alog := &LogA{
    		Log: Log{
    			Kind: "access",
    		},
    	}
    	return alog
    }
    
    func newErrorLog() *LogE {
    	eLog := &LogE{
    		Log: Log{
    			Kind: "error",
    		},
    	}
    	return eLog
    }
    
    func ErrorAlert(err error) {
    	e := newErrorLog()
    	e.Logging(err, LogAlert)
    }
    
    func ErrorCritical(err error) {
    	e := newErrorLog()
    	e.Logging(err, LogCritical)
    }
    
    func ErrorWarn(err error) {
    	e := newErrorLog()
    	e.Logging(err, LogWarn)
    }
    
    func (e *LogE) Logging(err error, level string) {
    	if tools.IsProductionEnv() {
    		e.write(err, level)
    	} else {
    		e.write(err, level)
    	}
    }
    
    func (a *LogA) Logging(r *http.Request) {
    	if tools.IsProductionEnv() {
    		a.write(r)
    	} else {
    		a.write(r)
    	}
    }
    
    func (a *LogA) write(r *http.Request) {
    	today := time.Now().Format("20060102")
    
    	a.Level = LogInfo
    	a.Time = time.Now()
    	a.Address = r.RemoteAddr
    	a.Method = r.Method
    	a.Path = r.URL.Path
    
    	writeJsonFile(fmt.Sprintf("%s/log_%s.json", configs.ErrorLogDirectory, today), a)
    }
    
    func (e *LogE) write(err error, level string) {
    	today := time.Now().Format("20060102")
    
    	e.Level = level
    	e.Content = err.Error()
    
    	// auto
    	_, file, line, _ := runtime.Caller(3))
    	e.Place = fmt.Sprintf("%s:%d", file, line)
    	e.Time = time.Now()
    
    	writeJsonFile(fmt.Sprintf("%s/log_%s.json", configs.ErrorLogDirectory, today), e)
    }
    
    // 上で解説したjson file書き込み関数
    func writeJsonFile(fileName string, object interface{}) {
    	file, _ := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0600)
    	defer file.Close()
    	fi, _ := file.Stat()
    	leng := fi.Size()
    
    	json_, _ := json.Marshal(object)
    
    	if leng == 0 {
    		file.Write([]byte(fmt.Sprintf(`[%s]`, json_)))
    	} else {
    		file.WriteAt([]byte(fmt.Sprintf(`,%s]`, json_)), leng-1)
    	}
    }
    
    func HttpLog(h http.Handler, l *LogA) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		l.Logging(r)
    		h.ServeHTTP(w, r)
    	})
    }
    
    상세히 설명하다.
    오류 로그나 액세스 로그에 Log 구성체가 포함된 구성체를 정의합니다.
    // Logの基底構造体
    type Log struct {
    	Kind  string    `json:"kind"`
    	Time  time.Time `json:"time"`
    	Level string    `json:"level"`
    }
    
    // Error用ログの構造体
    type LogE struct {
    	Log
    	Content string `json:"content"` // エラー内容
    	Place   string `json:"place"` // エラー発生場所
    }
    
    // Access用ログの構造体
    type LogA struct {
    	Log
    	Address string `json:"address"` // IPアドレス
    	Method  string `json:"method"` // request method
    	Path    string `json:"path"` // path
    }
    
    를 초기화할 때,kind는 기본값을 가지고 있습니다.
    func NewAccessLog() *LogA {
    	// startのlogをここで作る
    	alog := &LogA{
    		Log: Log{
    			Kind: "access",
    		},
    	}
    	return alog
    }
    
    func newErrorLog() *LogE {
    	eLog := &LogE{
    		Log: Log{
    			Kind: "error",
    		},
    	}
    	return eLog
    }
    
    오류 로그는 이렇게 하면 바로 호출할 수 있습니다.
    func ErrorAlert(err error) {
    	e := newErrorLog()
    	e.Logging(err, LogAlert)
    }
    
    func ErrorCritical(err error) {
    	e := newErrorLog()
    	e.Logging(err, LogCritical)
    }
    
    func ErrorWarn(err error) {
    	e := newErrorLog()
    	e.Logging(err, LogWarn)
    }
    
    func (e *LogE) Logging(err error, level string) {
    	if tools.IsProductionEnv() {
    		e.write(err, level)
    	} else {
    		e.write(err, level)
    	}
    }
    
    func (e *LogE) write(err error, level string) {
    	today := time.Now().Format("20060102")
    
    	e.Level = level
    	e.Content = err.Error()
    
    	// auto
    	_, file, line, _ := runtime.Caller(2)
    	e.Place = fmt.Sprintf("%s:%d", file, line)
    	e.Time = time.Now()
    
    	// 前章で解説してたjsonファイル書き込み関数
    	writeJsonFile(fmt.Sprintf("%s/log_%s.json", configs.ErrorLogDirectory, today), e)
    }
    
    다른 한편, 방문 로그는 매 세션의 사용 대상의 형식으로 사용된다.
    func (a *LogA) Logging(r *http.Request) {
    	if tools.IsProductionEnv() {
    		a.write(r)
    	} else {
    		a.write(r)
    	}
    }
    
    func (a *LogA) write(r *http.Request) {
    	today := time.Now().Format("20060102")
    
    	a.Level = LogInfo
    	a.Time = time.Now()
    	a.Address = r.RemoteAddr
    	a.Method = r.Method
    	a.Path = r.URL.Path
    
    	// 前章で解説してたjsonファイル書き込み関数
    	writeJsonFile(fmt.Sprintf("%s/log_%s.json", configs.ErrorLogDirectory, today), a)
    }
    
    // アクセスに関するログを吐き出す関数
    func HttpLog(h http.Handler, l *LogA) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		l.Logging(r)
    		h.ServeHTTP(w, r)
    	})
    }
    
    는 실제로 이렇게 사용한다.
    main.go
    package main
    
    import (
    	"net/http"
    	".../domain"
    )
    
    func main() {
    	router := http.NewServeMux()
    	l := domain.NewAccessLog()
    
    	router.Handle(...)
    
    	...
    
    	// リクエストごとにHttpLogが発動します。
    	if err := http.ListenAndServe(":8000", domain.HttpLog(router, l)); err != nil {
    		domain.ErrorAlert(err)
    	}
    }
    
    출력 결과는 이런 느낌입니다.(jq)
    [
      {
        "kind": "error",
        "time": "2021-08-05T07:38:54.051566671+09:00",
        "level": "WAN",
        "content": "sql: Rows are closed",
        "place": "<秘密>/internal/pkg/interfaces/database/review_repository.go:74"
      },
      {
        "kind": "access",
        "time": "2021-08-05T07:41:00.609925853+09:00",
        "level": "INF",
        "address": "52.00.000.000",
        "method": "GET",
        "path": "/db/anime/"
      }
    ]
    

    총결산


    goo에서 로그 파일을 출력하는 방법을 소개합니다.
    json 형식으로 로그를 출력하는 방법을 한층 더 소개했다.
    앞으로 방문 로그를 추가하여 등급이 높을 때 슬랙 알림을 즉시 발송하는 기능으로 일괄 처리를 통해 정기적으로 회수하는 로그를 sentry처럼 화면에서 볼 수 있습니다.
    결국 sentry를 사용하기로 결정해도 이렇게 스스로 만들어 보면서 많은 것을 배우는 것이 재미있다.
    이렇게 하면 자유롭게 실험을 할 수 있다는 것이 개인 개발의 장점이다.

    보충 덮어쓰기와 기준 비교


    덮어쓰기 방법
    원래 쓰는 법 자체가 묘한 것 같아요.
    좀 더 깔끔하게 쓸 수 있을 것 같아요.
    overrite.go
    func overWrite(fileName string, object interface{}) {
    	file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0600)
    	if err != nil {
    		fmt.Print(err)
    	}
    	defer file.Close()
    ​
    	buf := make([]byte, 1024*1024)
    	for {
    		n, err := file.Read(buf)
    		if n == 0 {
    			break
    		}
    		if err != nil {
    			panic(err)
    		}
    	}
    	// resize
    	idx := bytes.IndexByte(buf, 0)
    	buf = buf[:idx]var lst []interface{}
    	json.Unmarshal(buf, &lst)
    	lst = append(lst, object)
    	json_, _ := json.Marshal(lst)
    	file.WriteAt(json_, 0)
    }
    
    현재 하고 있는 일로 파일에 적힌 부분을 json에서 슬라이스로 바꾸어 새로운 요소를 적용합니다.
    그리고 그것을 json으로 만들어서 파일에 덮어씁니다.이런 방법.
    왠지 멋있어요.

    데이텀 측정의 결과도 아래에 나열됩니다.
    큰 차이는 없지만 새 파일에도 기록을 추가하는 방법이 성능이 더 좋은 것 같다.

    jsonwrite_test.go
    package jsonwrite_test
    ​
    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"os"
    	"testing"
    )// 要素を追加 (既存)
    func BenchmarkAppend_AppendJson(b *testing.B) {
    	b.ResetTimer()
    	p := &Sample{Name: "Jhone Doe", Old: 20, Explain: "aaaa aaaa"}
    	appendJson(filePath, p)
    }// まるごと書き換え (既存)
    func BenchmarkAppend_Overwrite(b *testing.B) {
    	b.ResetTimer()
    	p := &Sample{Name: "Jhone Doe", Old: 20, Explain: "aaaa aaaa"}
    	overWrite(filePath2, p)
    }// 要素を追加 (新規)
    func BenchmarkAppend_AppendJsonNew(b *testing.B) {
    	b.ResetTimer()
    	p := &Sample{Name: "Jhone Doe", Old: 20, Explain: "aaaa aaaa"}
    	appendJson(noFilePath, p)
    }// まるごと書き換え (新規)
    func BenchmarkAppend_OverwriteNew(b *testing.B) {
    	b.ResetTimer()
    	p := &Sample{Name: "Jhone Doe", Old: 20, Explain: "aaaa aaaa"}
    	overWrite(noFilePath2, p)
    }// delete file
    func BenchmarkAppend_DeleteFile(b *testing.B) {
    	b.ResetTimer()
    	os.Remove(noFilePath)
    	os.Remove(noFilePath2)
    }const filePath = "./sample.json"
    const noFilePath = "./no.json"
    const filePath2 = "./sample2.json"
    const noFilePath2 = "./no2.json"type Sample struct {
    	Name    string `json:"name"`
    	Old     int    `json:"old,omitempty"`
    	Explain string `json:"explain"`
    }func appendJson(fileName string, object interface{}) {
    	file, _ := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0600)
    	defer file.Close()
    	fi, _ := file.Stat()
    	leng := fi.Size()
    ​
    	json_, _ := json.Marshal(object)if leng == 0 {
    		file.Write([]byte(fmt.Sprintf(`[%s]`, json_)))
    	} else {
    		file.WriteAt([]byte(fmt.Sprintf(`,%s]`, json_)), leng-1)
    	}
    }func overWrite(fileName string, object interface{}) {
    	file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0600)
    	if err != nil {
    		fmt.Print(err)
    	}
    	defer file.Close()
    ​
    	buf := make([]byte, 1024*1024)
    	for {
    		n, err := file.Read(buf)
    		if n == 0 {
    			break
    		}
    		if err != nil {
    			panic(err)
    		}
    	}
    	// resize
    	idx := bytes.IndexByte(buf, 0)
    	buf = buf[:idx]var lst []interface{}
    	json.Unmarshal(buf, &lst)
    	lst = append(lst, object)
    	json_, _ := json.Marshal(lst)
    	file.WriteAt(json_, 0)
    }// result
    /******************************
    BenchmarkAppend_AppendJson-8            1000000000               0.001882 ns/op        0 B/op          0 allocs/op
    BenchmarkAppend_Overwrite-8             1000000000               0.004287 ns/op        0 B/op          0 allocs/op
    BenchmarkAppend_AppendJsonNew-8         1000000000               0.002100 ns/op        0 B/op          0 allocs/op
    BenchmarkAppend_OverwriteNew-8          1000000000               0.003463 ns/op        0 B/op          0 allocs/op
    ******************************/

    참고 자료 등


    https://qiita.com/nanasess/items/350e59b29cceb2f122b3
    https://zenn.dev/spiegel/books/error-handling-in-golang

    좋은 웹페이지 즐겨찾기