Skip to content

Commit

Permalink
feat(cloudrequestlog): migrate HTTP requests to slog
Browse files Browse the repository at this point in the history
Part of ongoing migration from zap to slog.
  • Loading branch information
odsod committed Oct 8, 2024
1 parent d5b1bcb commit c471688
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 65 deletions.
63 changes: 15 additions & 48 deletions cloudrequestlog/additionalfields.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ func (m *AdditionalFields) AppendTo(fields []zap.Field) []zap.Field {
return fields
}

func (m *AdditionalFields) appendTo(attrs []slog.Attr) []slog.Attr {
m.mu.Lock()
defer m.mu.Unlock()
for _, field := range m.fields {
attrs = append(attrs, fieldToAttr(field))
}
for _, array := range m.arrays {
attrs = append(attrs, slog.Any(array.key, array.values))
}
return attrs
}

type anyArray []any

func (oa anyArray) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
Expand All @@ -86,7 +98,7 @@ func argsToFieldSlice(args []any) []zap.Field {
fields := make([]zap.Field, 0, len(args))
for len(args) > 0 {
attr, args = argsToAttr(args)
fields = append(fields, convertAttrToField(attr))
fields = append(fields, attrToField(attr))
}
return fields
}
Expand All @@ -100,56 +112,11 @@ func argsToAttr(args []any) (slog.Attr, []any) {
return slog.String(badKey, x), nil
}
return slog.Any(x, args[1]), args[2:]
case zapcore.Field:
return fieldToAttr(x), args[1:]
case slog.Attr:
return x, args[1:]
default:
return slog.Any(badKey, x), args[1:]
}
}

// convertAttrToField is copied from go.uber.org/zap/exp/zapslog.
func convertAttrToField(attr slog.Attr) zap.Field {
if attr.Equal(slog.Attr{}) {
// Ignore empty attrs.
return zap.Skip()
}
switch attr.Value.Kind() {
case slog.KindBool:
return zap.Bool(attr.Key, attr.Value.Bool())
case slog.KindDuration:
return zap.Duration(attr.Key, attr.Value.Duration())
case slog.KindFloat64:
return zap.Float64(attr.Key, attr.Value.Float64())
case slog.KindInt64:
return zap.Int64(attr.Key, attr.Value.Int64())
case slog.KindString:
return zap.String(attr.Key, attr.Value.String())
case slog.KindTime:
return zap.Time(attr.Key, attr.Value.Time())
case slog.KindUint64:
return zap.Uint64(attr.Key, attr.Value.Uint64())
case slog.KindGroup:
if attr.Key == "" {
// Inlines recursively.
return zap.Inline(groupObject(attr.Value.Group()))
}
return zap.Object(attr.Key, groupObject(attr.Value.Group()))
case slog.KindLogValuer:
return convertAttrToField(slog.Attr{
Key: attr.Key,
Value: attr.Value.Resolve(),
})
default:
return zap.Any(attr.Key, attr.Value.Any())
}
}

// groupObject holds all the Attrs saved in a slog.GroupValue.
type groupObject []slog.Attr

func (gs groupObject) MarshalLogObject(enc zapcore.ObjectEncoder) error {
for _, attr := range gs {
convertAttrToField(attr).AddTo(enc)
}
return nil
}
36 changes: 19 additions & 17 deletions cloudrequestlog/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cloudrequestlog
import (
"context"
"errors"
"log/slog"
"net/http"
"reflect"
"runtime"
Expand All @@ -12,10 +13,12 @@ import (
"go.einride.tech/cloudrunner/cloudzap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
ltype "google.golang.org/genproto/googleapis/logging/type"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
)

// Middleware for request logging.
Expand Down Expand Up @@ -194,38 +197,37 @@ func measureHeaderSize(h http.Header) int {
// HTTPServer provides request logging for HTTP servers.
func (l *Middleware) HTTPServer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
responseWriter := &httpResponseWriter{ResponseWriter: w}
startTime := time.Now()
ctx := WithAdditionalFields(r.Context())
r = r.WithContext(ctx)
startTime := time.Now()
responseWriter := &httpResponseWriter{ResponseWriter: w}
next.ServeHTTP(responseWriter, r)
checkedEntry := l.logger(ctx).Check(
l.statusToLevel(responseWriter.Status()),
httpServerLogMessage(responseWriter, r),
)
if checkedEntry == nil {
level := l.statusToLevel(responseWriter.Status())
logger := slog.Default()
if !logger.Enabled(ctx, levelToSlog(level)) {
return
}
httpRequest := cloudzap.HTTPRequestObject{
logMessage := httpServerLogMessage(responseWriter, r)
httpRequest := &ltype.HttpRequest{
RequestMethod: r.Method,
Status: responseWriter.Status(),
ResponseSize: responseWriter.size + measureHeaderSize(w.Header()),
Status: int32(responseWriter.Status()),
ResponseSize: int64(responseWriter.size + measureHeaderSize(w.Header())),
UserAgent: r.UserAgent(),
RemoteIP: r.RemoteAddr,
RemoteIp: r.RemoteAddr,
Referer: r.Referer(),
Latency: time.Since(startTime),
Latency: durationpb.New(time.Since(startTime)),
Protocol: r.Proto,
}
if r.URL != nil {
httpRequest.RequestURL = r.URL.String()
httpRequest.RequestUrl = r.URL.String()
}
fields := []zapcore.Field{
cloudzap.HTTPRequest(&httpRequest),
attrs := []slog.Attr{
slog.Any("httpRequest", &httpRequest),
}
if additionalFields, ok := GetAdditionalFields(ctx); ok {
fields = additionalFields.AppendTo(fields)
attrs = additionalFields.appendTo(attrs)
}
checkedEntry.Write(fields...)
logger.LogAttrs(ctx, levelToSlog(level), logMessage, attrs...)
})
}

Expand Down
116 changes: 116 additions & 0 deletions cloudrequestlog/migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package cloudrequestlog

import (
"log/slog"
"math"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func slogToLevel(l slog.Level) zapcore.Level {
switch {
case l >= slog.LevelError:
return zapcore.ErrorLevel
case l >= slog.LevelWarn:
return zapcore.WarnLevel
case l >= slog.LevelInfo:
return zapcore.InfoLevel
default:
return zapcore.DebugLevel
}
}

func levelToSlog(l zapcore.Level) slog.Level {
switch l {
case zapcore.DebugLevel:
return slog.LevelDebug
case zapcore.InfoLevel:
return slog.LevelInfo
case zapcore.WarnLevel:
return slog.LevelWarn
case zapcore.ErrorLevel, zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
return slog.LevelError
default:
return slog.LevelDebug
}
}

func attrToField(attr slog.Attr) zapcore.Field {
if attr.Equal(slog.Attr{}) {
// Ignore empty attrs.
return zap.Skip()
}
switch attr.Value.Kind() {
case slog.KindBool:
return zap.Bool(attr.Key, attr.Value.Bool())
case slog.KindDuration:
return zap.Duration(attr.Key, attr.Value.Duration())
case slog.KindFloat64:
return zap.Float64(attr.Key, attr.Value.Float64())
case slog.KindInt64:
return zap.Int64(attr.Key, attr.Value.Int64())
case slog.KindString:
return zap.String(attr.Key, attr.Value.String())
case slog.KindTime:
return zap.Time(attr.Key, attr.Value.Time())
case slog.KindUint64:
return zap.Uint64(attr.Key, attr.Value.Uint64())
case slog.KindGroup:
if attr.Key == "" {
// Inlines recursively.
return zap.Inline(groupObject(attr.Value.Group()))
}
return zap.Object(attr.Key, groupObject(attr.Value.Group()))
case slog.KindLogValuer:
return attrToField(slog.Attr{
Key: attr.Key,
// TODO: resolve the value in a lazy way.
// This probably needs a new Zap field type
// that can be resolved lazily.
Value: attr.Value.Resolve(),
})
default:
return zap.Any(attr.Key, attr.Value.Any())
}
}

// groupObject holds all the Attrs saved in a slog.GroupValue.
type groupObject []slog.Attr

func (gs groupObject) MarshalLogObject(enc zapcore.ObjectEncoder) error {
for _, attr := range gs {
attrToField(attr).AddTo(enc)
}
return nil
}

func fieldToAttr(field zapcore.Field) slog.Attr {
switch field.Type {
case zapcore.StringType:
return slog.String(field.Key, field.String)
case zapcore.Int64Type:
return slog.Int64(field.Key, field.Integer)
case zapcore.Int32Type:
return slog.Int(field.Key, int(field.Integer))
case zapcore.Uint64Type:
return slog.Uint64(field.Key, uint64(field.Integer))
case zapcore.Float64Type:
return slog.Float64(field.Key, math.Float64frombits(uint64(field.Integer)))
case zapcore.BoolType:
return slog.Bool(field.Key, field.Integer == 1)
case zapcore.TimeType:
if field.Interface != nil {
loc, ok := field.Interface.(*time.Location)
if ok {
return slog.Time(field.Key, time.Unix(0, field.Integer).In(loc))
}
}
return slog.Time(field.Key, time.Unix(0, field.Integer))
case zapcore.DurationType:
return slog.Duration(field.Key, time.Duration(field.Integer))
default:
return slog.Any(field.Key, field.Interface)
}
}
27 changes: 27 additions & 0 deletions examples/cmd/http-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"context"
"fmt"
"log/slog"
"net/http"
"os"

"go.einride.tech/cloudrunner"
)

func main() {
if err := cloudrunner.Run(func(ctx context.Context) error {
cloudrunner.Logger(ctx).Info("hello world")
httpServer := cloudrunner.NewHTTPServer(ctx, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(ctx, "hello from handler")
cloudrunner.AddRequestLogFields(r.Context(), "foo", "bar")
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte("hello world"))
}))
return cloudrunner.ListenHTTP(ctx, httpServer)
}); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

0 comments on commit c471688

Please sign in to comment.