ロギング

概要

ログ関連機能を提供します。

ログに関する機能はzlogパッケージにより提供されています。

機能

1. ログ出力機能

ログ出力機能は、ログを出力する機能です。 zlogパッケージでは以下のようにロガーのインターフェースが定められています。 これは特定のログパッケージに依存しないインターフェースとなっています。

type Logger interface {
	DebugEnabled(ctx context.Context) bool
	InfoEnabled(ctx context.Context) bool
	WarnEnabled(ctx context.Context) bool
	ErrorEnabled(ctx context.Context) bool
	DebugContext(ctx context.Context, msg string, args ...any)
	InfoContext(ctx context.Context, msg string, args ...any)
	WarnContext(ctx context.Context, msg string, args ...any)
	ErrorContext(ctx context.Context, msg string, args ...any)
}

zlog/zslogパッケージは、Go 標準パッケージのlog/slog.Loggerにたいしてロガーインターフェースを実装する機能を提供しています。

以下はデフォルトのslog.Loggerに対して上記の Logger インターフェースを実装する例です。

package main

import (
	"context"
	"log/slog"

	"github.com/aileron-projects/go/zlog/zslog"
)

func main() {
	// Create logger from default slogger handler.
	lg := zslog.New(slog.Default().Handler())

	ctx := context.Background()
	if lg.DebugEnabled(ctx) {
		lg.DebugContext(ctx, "debug enabled")
	}
	if lg.InfoEnabled(ctx) {
		lg.InfoContext(ctx, "info enabled")
	}
	if lg.WarnEnabled(ctx) {
		lg.WarnContext(ctx, "warn enabled")
	}
	if lg.ErrorEnabled(ctx) {
		lg.ErrorContext(ctx, "error enabled")
	}
}

2. コンテキスト属性操作機能

コンテキスト属性操作機能はcontext.Contextにログの属性を保存・取得する機能です。 これによりロガーはコンテキストに紐づく属性をログに含めることが可能です。

属性をコンテキストに保存・取得するためにContextWithAttrsAttrsFromContextの2つの関数が提供されています。

ctx := context.Background()

// Save attributes in the context.
ctx = zlog.ContextWithAttrs(ctx, "key", "value")

// Extract attributes from the context.
attrs := zlog.AttrsFromContext(ctx)

なお、zlog/zslog.Newで作成したロガーを利用した場合、コンテキストに含まれる属性は自動的にログに出力されます。

以下のコードで動作確認ができます。

package main

import (
	"context"
	"log/slog"

	"github.com/aileron-projects/go/zlog"
	"github.com/aileron-projects/go/zlog/zslog"
)

func main() {
	lg := zslog.New(slog.Default().Handler())

	ctx := context.Background()
	ctx = zlog.ContextWithAttrs(ctx, "key", "value")

	if lg.InfoEnabled(ctx) {
		lg.InfoContext(ctx, "output context attributes")
	}
}

3. コンテキストログレベル設定機能

コンテキストログレベル設定機能はcontext.Contextを利用して、対象となるコンテキストに対してログ出力レベルを設定する機能です。 利用しているロガーのログレベルが Info である場合でも、コンテキストのログレベルを Debug に設定すると、当該コンテキストに紐づけられたログレコードは Debug レベルで出力されます。

この機能はデバッグや、コンテキストに応じたログレベルによるログ出力に利用できます。

以下のコードでは、ロガーのログレベルはInfoになっています。 しかし、コンテキストはDebugレベルを指定しているため、Debug ログが出力されます。

ログレベルをコンテキストに設定するためにContextWithLevelLevelFromContextの2つの関数が提供されています。

ctx := context.Background()

// Save log level in the context.
ctx = zslog.ContextWithLevel(ctx, slog.LevelDebug)

// Extract log level from the context.
lv := zslog.LevelFromContext(ctx)

なお、zlog/zslog.Newで作成したロガーを利用した場合、コンテキストに設定されたログレベルは自動的に認識されるようになります。

以下のコードで動作確認ができます。

package main

import (
	"context"
	"log/slog"

	"github.com/aileron-projects/go/zlog/zslog"
)

func main() {
	lg := zslog.New(slog.Default().Handler()) // Info level logger.
	ctx := context.Background()

	// Try to output debug log.
	lg.DebugContext(ctx, "log should not be output")

	// Set this context to debug level.
	ctx = zslog.ContextWithLevel(ctx, slog.LevelDebug)

	// Once again, try to output debug log.
	lg.DebugContext(ctx, "log should be output")
}

4. 論理ファイル機能

論理ファイル機能は、物理ファイルを仮想的にサイズ無限のファイルとして扱う機能です。

ログのファイル出力においてはログファイルのローテーションや世代管理、ログファイル圧縮などを考慮する必要があります。 論理ファイルにより、ロガー自身がこれら物理ファイルの存在や管理を意識する必要がなくなります。 これは Linux における論理ボリューム管理(LVM)に似ています。

論理ファイル機能は、アクティブな1つの物理ファイルと、複数の履歴ファイルを管理します。 履歴ファイルのファイル名は固定値(例えば app.log)であり、履歴ファイルにはカウンター値や日時を含みます(例えば app-20060102-150405.log)。

履歴ファイルのファイル名に指定できるフォーマット指定子は以下の表のとおりです。 フォーマット指定子が指定されていない場合、ファイル管理のために自動的に%iが付与されます。 なお、MaxAgeによるファイル管理を行う場合、日時を含むフォーマット指定子をファイル名に含む必要があります。

FormatValueRange
%YYYYY 4 digits year0 <= YYYY
%MMM 2 digits month1 <= MM <= 12
%DDD 2 digits day of month1 <= DD <= 31
%hhh 2 digits hour0 <= hh <= 23
%mmm 2 digits minute0 <= mm <= 59
%sss 2 digits second0 <= ss <= 59
%uunix second with free digits0 <= unix
%iindex with free digits0 <= index
%Hhostname
%Uuser id. “-1” on windows
%Guser group id. “-1” on windows
%ppid (process id)
%Pppid (parent process id)

論理ファイル機能は以下の項目で物理ファイルを管理します。 これらは組み合わせて利用することも可能です。

  • MaxAge: 指定時間より古いファイルを削除 (%Y, %M, %D の全て、または %u が必須)
  • MaxHistory: 履歴ファイルが指定数より多い分を削除
  • MaxTotal: ファイルの合計サイズが指定値を超えた場合に削除

また、履歴ファイルはGzip圧縮により圧縮することも可能です。

セキュリティに関する特記事項

ログ関連機能ではログマスク機能を持ちません。 必要に応じてユーザ側で実装してください。

性能に関する特記事項

ログ出力は必ずしもifで囲む必要はありません。 可読性と性能を考慮して使い分けることを推奨します。 Debugレベルのログはほとんどの場合、本番環境では出力されないのにくわえて出力項目も多くなりがちのため if 文で囲むことが推奨されます。 一方で、Errorレベルのログはほとんどの場合、本番環境で出力されるため if 文の利用は冗長です。

lg.InfoContext(ctx, "log message")
if lg.InfoEnabled(ctx) {
	lg.InfoContext(ctx, "log message")
}

実装例・使い方

MaxAgeによるファイル管理

以下の実装例はMaxAgeにより履歴管理をおこないます。 1つのファイルのサイズが500バイトを超えないように物理ファイルがローテーションされ、 30秒前より古いファイルは削除されます。

履歴ファイルのファイル名に含まれる%uはUnix秒を表しています。 なお、MaxAgeを利用する場合は、最低限%Y/%M/%Dのすべてまたは%uを履歴ファイル名に含む必要があります。 時間指定子(%h/%m/%s)が含まれない場合、それらは値がゼロ(00時/00分/00秒)として扱われます。

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6
 7	"github.com/aileron-projects/go/zlog"
 8)
 9
10func main() {
11	c := &zlog.LogicalFileConfig{
12		Manager: &zlog.FileManagerConfig{
13			MaxAge:  30 * time.Second,
14			Pattern: "app.%u.log",
15		},
16		RotateBytes: 500,       // Max size of a single file.
17		FileName:    "app.log", // Active file name.
18	}
19	f, err := zlog.NewLogicalFile(c)
20	if err != nil {
21		panic(err)
22	}
23
24	initial := time.Now()
25	for {
26		fmt.Println("Now:", time.Now().Unix(), "\t", "30s before:", time.Now().Unix()-30)
27		fmt.Fprintln(f, time.Now(), "Time past ", time.Since(initial))
28		time.Sleep(time.Second)
29	}
30}

MaxHistoryによるファイル管理

以下の実装例はMaxHistoryにより履歴管理をおこないます。 1つのファイルのサイズが500バイトを超えないように物理ファイルがローテーションされます。 履歴ファイルの数が5つより多くならないように、古いファイルを削除します。

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6
 7	"github.com/aileron-projects/go/zlog"
 8)
 9
10func main() {
11	c := &zlog.LogicalFileConfig{
12		Manager: &zlog.FileManagerConfig{
13			MaxHistory: 5,
14			Pattern:    "app.%i.log",
15		},
16		RotateBytes: 500,       // Max size of a single file.
17		FileName:    "app.log", // Active file name.
18	}
19	f, err := zlog.NewLogicalFile(c)
20	if err != nil {
21		panic(err)
22	}
23
24	initial := time.Now()
25	for {
26		fmt.Fprintln(f, time.Now(), "Time past ", time.Since(initial))
27		time.Sleep(time.Second)
28	}
29}

MaxTotalによるファイル管理

以下の実装例はMaxTotalBytesにより履歴管理をおこないます。 1つのファイルのサイズが500バイトを超えないように物理ファイルがローテーションされます。 全ての履歴ファイルのファイルサイズの合計が数が2000バイトより大きくならないように、古いファイルから削除します。

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6
 7	"github.com/aileron-projects/go/zlog"
 8)
 9
10func main() {
11	c := &zlog.LogicalFileConfig{
12		Manager: &zlog.FileManagerConfig{
13			MaxTotalBytes: 2000,
14			Pattern:       "app.%i.log",
15		},
16		RotateBytes: 500,       // Max size of a single file.
17		FileName:    "app.log", // Active file name.
18	}
19	f, err := zlog.NewLogicalFile(c)
20	if err != nil {
21		panic(err)
22	}
23
24	initial := time.Now()
25	for {
26		fmt.Fprintln(f, time.Now(), "Time past ", time.Since(initial))
27		time.Sleep(time.Second)
28	}
29}

ログのファイル出力

論理ファイルはio.Writerのインターフェースを実装しているため、 ロガーの出力先として利用することが可能です。

Goの標準パッケージであるlog/slog.Handlerと組み合わせて利用する場合の利用例を以下に示します。 このような実装により、履歴管理機能付きのロギングを実現することが可能です。

package main

import (
	"context"
	"log/slog"
	"time"

	"github.com/aileron-projects/go/zlog"
	"github.com/aileron-projects/go/zlog/zslog"
)

func main() {
	c := &zlog.LogicalFileConfig{
		Manager: &zlog.FileManagerConfig{
			MaxHistory: 5,
			Pattern:    "app.%i.log",
		},
		RotateBytes: 500,       // Max size of a single file.
		FileName:    "app.log", // Active file name.
	}
	f, err := zlog.NewLogicalFile(c)
	if err != nil {
		panic(err)
	}

	h := slog.NewTextHandler(f, nil) // Create slog handler with f.
	lg := zslog.New(h)
	for {
		lg.InfoContext(context.Background(), "log message", "now", time.Now())
		time.Sleep(time.Second)
	}
}

参考資料