Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add std logger #95

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions internal/log/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package log

import (
"context"
"fmt"
"io"
"log/slog"
"sync"
"time"
)

// ValueTextHandler is a [Handler] that writes Records to an [io.Writer] with values separated by spaces.
type ValueTextHandler struct {
h slog.Handler
mu *sync.Mutex
out io.Writer
}

// NewValueTextHandler creates a [ValueTextHandler] that writes to out, using the given options.
func NewValueTextHandler(out io.Writer, opts *slog.HandlerOptions) *ValueTextHandler {
if opts == nil {
opts = &slog.HandlerOptions{}
}
return &ValueTextHandler{
out: out,
h: slog.NewTextHandler(out, &slog.HandlerOptions{
Level: opts.Level,
AddSource: opts.AddSource,
ReplaceAttr: nil,
}),
mu: &sync.Mutex{},
}
}

func (h *ValueTextHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.h.Enabled(ctx, level)
}

func (h *ValueTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &ValueTextHandler{h: h.h.WithAttrs(attrs), out: h.out, mu: h.mu}
}

func (h *ValueTextHandler) WithGroup(name string) slog.Handler {
return &ValueTextHandler{h: h.h.WithGroup(name), out: h.out, mu: h.mu}
}

// Handle formats its argument [Record] as a single line of space-separated values.
// Example output format: 2024-11-12T15:08:11.451377-08:00 INFO "Initalized 0 sources.\n"
func (h *ValueTextHandler) Handle(ctx context.Context, r slog.Record) error {
buf := make([]byte, 0, 1024)

// time
if !r.Time.IsZero() {
buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time))
}
// level
buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level))
// message
buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message))

r.Attrs(func(a slog.Attr) bool {
buf = h.appendAttr(buf, a)
return true
})
buf = append(buf, "\n"...)

h.mu.Lock()
defer h.mu.Unlock()
_, err := h.out.Write(buf)
return err
}

// appendAttr is reponsible for formatting a single attribute
func (h *ValueTextHandler) appendAttr(buf []byte, a slog.Attr) []byte {
// Resolve the Attr's value before doing anything else.
a.Value = a.Value.Resolve()
// Ignore empty Attrs.
if a.Equal(slog.Attr{}) {
return buf
}
switch a.Value.Kind() {
case slog.KindString:
// Quote string values, to make them easy to parse.
buf = fmt.Appendf(buf, "%q ", a.Value.String())
case slog.KindTime:
// Write times in a standard way, without the monotonic time.
buf = fmt.Appendf(buf, "%s ", a.Value.Time().Format(time.RFC3339Nano))
case slog.KindGroup:
attrs := a.Value.Group()
// Ignore empty groups.
if len(attrs) == 0 {
return buf
}
for _, ga := range attrs {
buf = h.appendAttr(buf, ga)
}
default:
buf = fmt.Appendf(buf, "%s ", a.Value)
}

return buf
}
91 changes: 91 additions & 0 deletions internal/log/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package log

import (
"fmt"
"io"
"log/slog"
"strings"

"github.com/googleapis/genai-toolbox/toolbox"
)

// StdLogger is the standard logger
type StdLogger struct {
outLogger *slog.Logger
errLogger *slog.Logger
}

// NewStdLogger create a Logger that uses out and err for informational and error messages.
func NewStdLogger(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)

handlerOptions := &slog.HandlerOptions{Level: programLevel}

return &StdLogger{
outLogger: slog.New(NewValueTextHandler(outW, handlerOptions)),
errLogger: slog.New(NewValueTextHandler(errW, handlerOptions)),
}, nil
}

// Debug logs debug messages
func (sl *StdLogger) Debug(msg string, keysAndValues ...interface{}) {
sl.outLogger.Debug(msg, keysAndValues...)
}

// Info logs debug messages
func (sl *StdLogger) Info(msg string, keysAndValues ...interface{}) {
sl.outLogger.Info(msg, keysAndValues...)
}

// Warn logs warning messages
func (sl *StdLogger) Warn(msg string, keysAndValues ...interface{}) {
sl.errLogger.Warn(msg, keysAndValues...)
}

// Error logs error messages
func (sl *StdLogger) Error(msg string, keysAndValues ...interface{}) {
sl.errLogger.Error(msg, keysAndValues...)
}

const (
Debug = "DEBUG"
Info = "INFO"
Warn = "WARN"
Error = "ERROR"
)

// Returns severity level based on string.
func severityToLevel(s string) (slog.Level, error) {
switch strings.ToUpper(s) {
case Debug:
return slog.LevelDebug, nil
case Info:
return slog.LevelInfo, nil
case Warn:
return slog.LevelWarn, nil
case Error:
return slog.LevelError, nil
default:
return slog.Level(-5), fmt.Errorf("invalid log level")
}
}
Loading
Loading