mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-09 15:38:08 -05:00
feat: add structured logger (#96)
Logging support 4 different types of logging (debug, info, warn, error).
Example of structured logger:
```
{
"timestamp":"2024-11-04T16:45:11.987299-08:00",
"severity":"ERROR",
"logging.googleapis.com/sourceLocation":{
"function":"github.com/googleapis/genai-toolbox/internal/log.(*StructuredLogger).Errorf",
"file":"/Users/yuanteoh/github/genai-toolbox/internal/log/log.go","line":157
},
"message":"unable to parse tool file at \"tools.yaml\": \"cloud-sql-postgres1\" is not a valid kind of data source"
}
```
```
{
"timestamp":"2024-11-04T16:45:11.987562-08:00",
"severity":"INFO",
"logging.googleapis.com/sourceLocation":{
"function":"github.com/googleapis/genai-toolbox/internal/log.(*StructuredLogger).Infof",
"file":"/Users/yuanteoh/github/genai-toolbox/internal/log/log.go","line":147
},
"message":"Initalized 0 sources.\n"
}
```
This commit is contained in:
@@ -89,3 +89,98 @@ func severityToLevel(s string) (slog.Level, error) {
|
||||
return slog.Level(-5), fmt.Errorf("invalid log level")
|
||||
}
|
||||
}
|
||||
|
||||
// Returns severity string based on level.
|
||||
func levelToSeverity(s string) (string, error) {
|
||||
switch s {
|
||||
case slog.LevelDebug.String():
|
||||
return Debug, nil
|
||||
case slog.LevelInfo.String():
|
||||
return Info, nil
|
||||
case slog.LevelWarn.String():
|
||||
return Warn, nil
|
||||
case slog.LevelError.String():
|
||||
return Error, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid slog level")
|
||||
}
|
||||
}
|
||||
|
||||
type StructuredLogger struct {
|
||||
outLogger *slog.Logger
|
||||
errLogger *slog.Logger
|
||||
}
|
||||
|
||||
// NewStructuredLogger create a Logger that logs messages using JSON.
|
||||
func NewStructuredLogger(outW, errW io.Writer, logLevel string) (toolbox.Logger, error) {
|
||||
//Set log level
|
||||
var programLevel = new(slog.LevelVar)
|
||||
slogLevel, err := severityToLevel(logLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
programLevel.Set(slogLevel)
|
||||
|
||||
replace := func(groups []string, a slog.Attr) slog.Attr {
|
||||
switch a.Key {
|
||||
case slog.LevelKey:
|
||||
value := a.Value.String()
|
||||
sev, _ := levelToSeverity(value)
|
||||
return slog.Attr{
|
||||
Key: "severity",
|
||||
Value: slog.StringValue(sev),
|
||||
}
|
||||
case slog.MessageKey:
|
||||
return slog.Attr{
|
||||
Key: "message",
|
||||
Value: a.Value,
|
||||
}
|
||||
case slog.SourceKey:
|
||||
return slog.Attr{
|
||||
Key: "logging.googleapis.com/sourceLocation",
|
||||
Value: a.Value,
|
||||
}
|
||||
case slog.TimeKey:
|
||||
return slog.Attr{
|
||||
Key: "timestamp",
|
||||
Value: a.Value,
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Configure structured logs to adhere to Cloud LogEntry format
|
||||
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
|
||||
outHandler := slog.NewJSONHandler(outW, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: programLevel,
|
||||
ReplaceAttr: replace,
|
||||
})
|
||||
errHandler := slog.NewJSONHandler(errW, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: programLevel,
|
||||
ReplaceAttr: replace,
|
||||
})
|
||||
|
||||
return &StructuredLogger{outLogger: slog.New(outHandler), errLogger: slog.New(errHandler)}, nil
|
||||
}
|
||||
|
||||
// Debug logs debug messages
|
||||
func (sl *StructuredLogger) Debug(msg string, keysAndValues ...interface{}) {
|
||||
sl.outLogger.Debug(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Info logs info messages
|
||||
func (sl *StructuredLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||
sl.outLogger.Info(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Warn logs warning messages
|
||||
func (sl *StructuredLogger) Warn(msg string, keysAndValues ...interface{}) {
|
||||
sl.errLogger.Warn(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// Error logs error messages
|
||||
func (sl *StructuredLogger) Error(msg string, keysAndValues ...interface{}) {
|
||||
sl.errLogger.Error(msg, keysAndValues...)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -72,6 +73,54 @@ func TestSeverityToLevelError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelToSeverity(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "test debug",
|
||||
in: slog.LevelDebug.String(),
|
||||
want: "DEBUG",
|
||||
},
|
||||
{
|
||||
name: "test info",
|
||||
in: slog.LevelInfo.String(),
|
||||
want: "INFO",
|
||||
},
|
||||
{
|
||||
name: "test warn",
|
||||
in: slog.LevelWarn.String(),
|
||||
want: "WARN",
|
||||
},
|
||||
{
|
||||
name: "test error",
|
||||
in: slog.LevelError.String(),
|
||||
want: "ERROR",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := levelToSeverity(tc.in)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("incorrect level to severity: got %v, want %v", got, tc.want)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelToSeverityError(t *testing.T) {
|
||||
_, err := levelToSeverity("fail")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error on incorrect slog level")
|
||||
}
|
||||
}
|
||||
|
||||
func runLogger(logger toolbox.Logger, logMsg string) {
|
||||
switch logMsg {
|
||||
case "info":
|
||||
@@ -234,3 +283,202 @@ func TestStdLogger(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructuredLoggerDebugLog(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
logLevel string
|
||||
logMsg string
|
||||
wantOut map[string]string
|
||||
wantErr map[string]string
|
||||
}{
|
||||
{
|
||||
name: "debug logger logging debug",
|
||||
logLevel: "debug",
|
||||
logMsg: "debug",
|
||||
wantOut: map[string]string{
|
||||
"severity": "DEBUG",
|
||||
"message": "log debug",
|
||||
},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "info logger logging debug",
|
||||
logLevel: "info",
|
||||
logMsg: "debug",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "warn logger logging debug",
|
||||
logLevel: "warn",
|
||||
logMsg: "debug",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "error logger logging debug",
|
||||
logLevel: "error",
|
||||
logMsg: "debug",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "debug logger logging info",
|
||||
logLevel: "debug",
|
||||
logMsg: "info",
|
||||
wantOut: map[string]string{
|
||||
"severity": "INFO",
|
||||
"message": "log info",
|
||||
},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "info logger logging info",
|
||||
logLevel: "info",
|
||||
logMsg: "info",
|
||||
wantOut: map[string]string{
|
||||
"severity": "INFO",
|
||||
"message": "log info",
|
||||
},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "warn logger logging info",
|
||||
logLevel: "warn",
|
||||
logMsg: "info",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "error logger logging info",
|
||||
logLevel: "error",
|
||||
logMsg: "info",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "debug logger logging warn",
|
||||
logLevel: "debug",
|
||||
logMsg: "warn",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{
|
||||
"severity": "WARN",
|
||||
"message": "log warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "info logger logging warn",
|
||||
logLevel: "info",
|
||||
logMsg: "warn",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{
|
||||
"severity": "WARN",
|
||||
"message": "log warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn logger logging warn",
|
||||
logLevel: "warn",
|
||||
logMsg: "warn",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{
|
||||
"severity": "WARN",
|
||||
"message": "log warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error logger logging warn",
|
||||
logLevel: "error",
|
||||
logMsg: "warn",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "debug logger logging error",
|
||||
logLevel: "debug",
|
||||
logMsg: "error",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{
|
||||
"severity": "ERROR",
|
||||
"message": "log error",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "info logger logging error",
|
||||
logLevel: "info",
|
||||
logMsg: "error",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{
|
||||
"severity": "ERROR",
|
||||
"message": "log error",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn logger logging error",
|
||||
logLevel: "warn",
|
||||
logMsg: "error",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{
|
||||
"severity": "ERROR",
|
||||
"message": "log error",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error logger logging error",
|
||||
logLevel: "error",
|
||||
logMsg: "error",
|
||||
wantOut: map[string]string{},
|
||||
wantErr: map[string]string{
|
||||
"severity": "ERROR",
|
||||
"message": "log error",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
outW := new(bytes.Buffer)
|
||||
errW := new(bytes.Buffer)
|
||||
|
||||
logger, err := NewStructuredLogger(outW, errW, tc.logLevel)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
runLogger(logger, tc.logMsg)
|
||||
|
||||
if len(tc.wantOut) != 0 {
|
||||
got := make(map[string]interface{})
|
||||
|
||||
if err := json.Unmarshal(outW.Bytes(), &got); err != nil {
|
||||
t.Fatalf("failed to parse writer")
|
||||
}
|
||||
|
||||
if got["severity"] != tc.wantOut["severity"] {
|
||||
t.Fatalf("incorrect severity: got %v, want %v", got["severity"], tc.wantOut["severity"])
|
||||
}
|
||||
|
||||
} else {
|
||||
if outW.String() != "" {
|
||||
t.Fatalf("incorrect log. got %v, want %v", outW.String(), "")
|
||||
}
|
||||
}
|
||||
|
||||
if len(tc.wantErr) != 0 {
|
||||
got := make(map[string]interface{})
|
||||
|
||||
if err := json.Unmarshal(errW.Bytes(), &got); err != nil {
|
||||
t.Fatalf("failed to parse writer")
|
||||
}
|
||||
|
||||
if got["severity"] != tc.wantErr["severity"] {
|
||||
t.Fatalf("incorrect severity: got %v, want %v", got["severity"], tc.wantErr["severity"])
|
||||
}
|
||||
|
||||
} else {
|
||||
if errW.String() != "" {
|
||||
t.Fatalf("incorrect log. got %v, want %v", errW.String(), "")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user