diff --git a/README.md b/README.md index 5a5c30cb..2a2c3710 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ opinionated, batteries-included service SDK. Run your application with [`cloudrunner.Run`](./run.go), and you get: - Logging integrated with [Cloud Logging](https://cloud.google.com/logging) - using [Zap](https://go.uber.org/zap). + using [slog](https://pkg.go.dev/log/slog). - Tracing integrated with [Cloud Trace](https://cloud.google.com/trace) using[OpenTelemetry Go](https://go.opentelemetry.io/otel). - Metrics integrated with @@ -41,6 +41,7 @@ package main import ( "context" "log" + "log/slog" "go.einride.tech/cloudrunner" "google.golang.org/grpc/health" @@ -49,7 +50,7 @@ import ( func main() { if err := cloudrunner.Run(func(ctx context.Context) error { - cloudrunner.Logger(ctx).Info("hello world") + slog.InfoContext(ctx, "hello world") grpcServer := cloudrunner.NewGRPCServer(ctx) healthServer := health.NewServer() grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) @@ -100,8 +101,10 @@ cloudrunner GOOGLE_CLOUD_PROJECT string cloudrunner RUNTIME_SERVICEACCOUNT string cloudrunner SERVICE_VERSION string cloudrunner ENABLE_PUBSUB_TRACING bool +cloudrunner LOGGER_PROJECTID string cloudrunner LOGGER_DEVELOPMENT bool true false -cloudrunner LOGGER_LEVEL zapcore.Level debug info +cloudrunner LOGGER_LEVEL slog.Level debug info +cloudrunner LOGGER_PROTOMESSAGESIZELIMIT int 1024 cloudrunner LOGGER_REPORTERRORS bool true cloudrunner PROFILER_ENABLED bool true cloudrunner PROFILER_MUTEXPROFILING bool diff --git a/cloudotel/errorhandler.go b/cloudotel/errorhandler.go index b4fc03bf..23a213d1 100644 --- a/cloudotel/errorhandler.go +++ b/cloudotel/errorhandler.go @@ -5,17 +5,8 @@ import ( "log/slog" "go.opentelemetry.io/otel" - "go.uber.org/zap" //nolint:gomodguard // legacy zap dependency for backwards compatibility - "go.uber.org/zap/zapcore" //nolint:gomodguard // legacy zap dependency for backwards compatibility ) -// NewErrorLogger returns a new otel.ErrorHandler that logs errors using the provided logger, level and message. -// -// Deprecated: This is a no-op as part of the migration from zap to slog. -func NewErrorLogger(*zap.Logger, zapcore.Level, string) otel.ErrorHandler { - return otel.ErrorHandlerFunc(func(error) {}) -} - // RegisterErrorHandler registers a global OpenTelemetry error handler. func RegisterErrorHandler(ctx context.Context) { otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { diff --git a/cloudotel/tracemiddleware.go b/cloudotel/tracemiddleware.go index 3dd3d400..4fec8d29 100644 --- a/cloudotel/tracemiddleware.go +++ b/cloudotel/tracemiddleware.go @@ -13,10 +13,8 @@ import ( gcppropagator "github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator" "go.einride.tech/cloudrunner/cloudpubsub" "go.einride.tech/cloudrunner/cloudstream" - "go.einride.tech/cloudrunner/cloudzap" //nolint:staticcheck // SA1019: internal use of deprecated package pending removal "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" //nolint:gomodguard // legacy zap dependency for trace middleware "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -131,19 +129,8 @@ func (i *TraceMiddleware) withLogTracing(ctx context.Context, spanCtx trace.Span if i.TraceHook != nil { ctx = i.TraceHook(ctx, spanCtx) } - fields := make([]zap.Field, 0, 3) - //nolint:staticcheck // SA1019: deprecated, pending removal - fields = append(fields, cloudzap.Trace(spanCtx.TraceID().String())) - if spanCtx.SpanID().String() != "" { - //nolint:staticcheck // SA1019: deprecated, pending removal - fields = append(fields, cloudzap.SpanID(spanCtx.SpanID().String())) - } - if spanCtx.IsSampled() { - //nolint:staticcheck // SA1019: deprecated, pending removal - fields = append(fields, cloudzap.TraceSampled(spanCtx.IsSampled())) - } - //nolint:staticcheck // SA1019: deprecated, pending removal - return cloudzap.WithLoggerFields(ctx, fields...) + // Trace fields are automatically added by the slog handler from the span context + return ctx } func propagatePubsubTracing(ctx context.Context, r *http.Request) context.Context { diff --git a/cloudrequestlog/additionalfields.go b/cloudrequestlog/additionalfields.go index f19a9442..17cb5762 100644 --- a/cloudrequestlog/additionalfields.go +++ b/cloudrequestlog/additionalfields.go @@ -4,8 +4,6 @@ import ( "context" "log/slog" "sync" - - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudrequestlog uses zap for legacy request logging ) type additionalFieldsKey struct{} @@ -88,8 +86,6 @@ 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: diff --git a/cloudrequestlog/details.go b/cloudrequestlog/details.go index 36d38ce8..6e88f8f2 100644 --- a/cloudrequestlog/details.go +++ b/cloudrequestlog/details.go @@ -2,46 +2,31 @@ package cloudrequestlog import ( "encoding/json" + "log/slog" - "go.uber.org/zap" //nolint:gomodguard // cloudrequestlog uses zap for legacy request logging - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudrequestlog uses zap for legacy request logging "google.golang.org/grpc/status" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" ) -// ErrorDetails creates a zap.Field that logs the gRPC error details of the provided error. -// -// Deprecated: Returns a zap.Field which ties consumers to the deprecated zap dependency. -// There is no drop-in slog replacement yet. This function will be removed in a future version. -func ErrorDetails(err error) zap.Field { +// ErrorDetails creates a slog.Attr that logs the gRPC error details of the provided error. +func ErrorDetails(err error) slog.Attr { if err == nil { - return zap.Skip() + return slog.Attr{} } s, ok := status.FromError(err) if !ok { - return zap.Skip() + return slog.Attr{} } protoDetails := s.Proto().GetDetails() if len(protoDetails) == 0 { - return zap.Skip() + return slog.Attr{} } - return zap.Array("errorDetails", errorDetailsMarshaler(protoDetails)) -} - -type errorDetailsMarshaler []*anypb.Any - -var _ zapcore.ArrayMarshaler = errorDetailsMarshaler{} - -// MarshalLogArray implements zapcore.ArrayMarshaler. -func (d errorDetailsMarshaler) MarshalLogArray(encoder zapcore.ArrayEncoder) error { - for _, detail := range d { - if err := encoder.AppendReflected(reflectProtoMessage{message: detail}); err != nil { - return err - } + details := make([]reflectProtoMessage, len(protoDetails)) + for i, detail := range protoDetails { + details[i] = reflectProtoMessage{message: detail} } - return nil + return slog.Any("errorDetails", details) } type reflectProtoMessage struct { diff --git a/cloudrequestlog/migration.go b/cloudrequestlog/migration.go deleted file mode 100644 index 0127e1ad..00000000 --- a/cloudrequestlog/migration.go +++ /dev/null @@ -1,38 +0,0 @@ -package cloudrequestlog - -import ( - "log/slog" - "math" - "time" - - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudrequestlog uses zap for legacy request logging -) - -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) - } -} diff --git a/cloudtrace/middleware.go b/cloudtrace/middleware.go index f75726d6..cb80ffd6 100644 --- a/cloudtrace/middleware.go +++ b/cloudtrace/middleware.go @@ -5,8 +5,6 @@ import ( "net/http" "go.einride.tech/cloudrunner/cloudstream" - "go.einride.tech/cloudrunner/cloudzap" //nolint:staticcheck // SA1019: internal use of deprecated package pending removal - "go.uber.org/zap" //nolint:gomodguard // legacy zap dependency for trace middleware "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -106,17 +104,6 @@ func (i *Middleware) withLogTracing(ctx context.Context, header string) context. if i.TraceHook != nil { ctx = i.TraceHook(ctx, traceContext) } - fields := make([]zap.Field, 0, 3) - //nolint:staticcheck // SA1019: deprecated, pending removal - fields = append(fields, cloudzap.Trace(traceContext.TraceID)) - if traceContext.SpanID != "" { - //nolint:staticcheck // SA1019: deprecated, pending removal - fields = append(fields, cloudzap.SpanID(traceContext.SpanID)) - } - if traceContext.Sampled { - //nolint:staticcheck // SA1019: deprecated, pending removal - fields = append(fields, cloudzap.TraceSampled(traceContext.Sampled)) - } - //nolint:staticcheck // SA1019: deprecated, pending removal - return cloudzap.WithLoggerFields(ctx, fields...) + // Trace fields are automatically added by the slog handler from the span context + return ctx } diff --git a/cloudzap/context.go b/cloudzap/context.go deleted file mode 100644 index 6bf9fe26..00000000 --- a/cloudzap/context.go +++ /dev/null @@ -1,36 +0,0 @@ -package cloudzap - -import ( - "context" - - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package -) - -type loggerContextKey struct{} - -// WithLogger adds a logger to the current context. -// -// Deprecated: cloudrunner.Run configures the default slog logger with cloudslog.Handler. -// Use slog.InfoContext, slog.WarnContext, etc. instead of a context-based zap logger. -func WithLogger(ctx context.Context, logger *zap.Logger) context.Context { - return context.WithValue(ctx, loggerContextKey{}, logger) -} - -// GetLogger returns the logger for the current context. -// -// Deprecated: Use slog.InfoContext, slog.WarnContext, etc. with the default slog logger instead. -func GetLogger(ctx context.Context) (*zap.Logger, bool) { - logger, ok := ctx.Value(loggerContextKey{}).(*zap.Logger) - return logger, ok -} - -// WithLoggerFields attaches structured fields to a new logger in the returned child context. -// -// Deprecated: Use cloudslog.With to attach attributes to the context instead. -func WithLoggerFields(ctx context.Context, fields ...zap.Field) context.Context { - logger, ok := ctx.Value(loggerContextKey{}).(*zap.Logger) - if !ok { - return ctx - } - return WithLogger(ctx, logger.With(fields...)) -} diff --git a/cloudzap/doc.go b/cloudzap/doc.go deleted file mode 100644 index 1547cb13..00000000 --- a/cloudzap/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package cloudzap provides primitives for structured logging with go.uber.org/zap. -// -// Deprecated: Use log/slog with the cloudslog package instead. The cloudslog.Handler -// automatically handles trace correlation, error reporting, and Cloud Logging field -// formatting without requiring explicit middleware or field injection. -package cloudzap diff --git a/cloudzap/encoderconfig.go b/cloudzap/encoderconfig.go deleted file mode 100644 index f092c444..00000000 --- a/cloudzap/encoderconfig.go +++ /dev/null @@ -1,33 +0,0 @@ -package cloudzap - -import ( - "fmt" - "time" - - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package -) - -// NewEncoderConfig creates a new zapcore.EncoderConfig for structured JSON logging to Cloud Logging. -// See: https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields. -// -// Deprecated: Use cloudslog.NewHandler instead, which handles Cloud Logging field formatting. -func NewEncoderConfig() zapcore.EncoderConfig { - return zapcore.EncoderConfig{ - TimeKey: "time", - LevelKey: "severity", - NameKey: "logger", - // Omit caller and log structured source location instead. - // See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogEntrySourceLocation - CallerKey: "", - MessageKey: "message", - StacktraceKey: "stacktrace", - LineEnding: zapcore.DefaultLineEnding, - EncodeTime: zapcore.RFC3339NanoTimeEncoder, - EncodeDuration: func(duration time.Duration, encoder zapcore.PrimitiveArrayEncoder) { - encoder.AppendString(fmt.Sprintf("%gs", duration.Seconds())) - }, - EncodeLevel: func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) { - encoder.AppendString(LevelToSeverity(level)) - }, - } -} diff --git a/cloudzap/errorreport.go b/cloudzap/errorreport.go deleted file mode 100644 index e05d6b65..00000000 --- a/cloudzap/errorreport.go +++ /dev/null @@ -1,77 +0,0 @@ -package cloudzap - -import ( - "runtime" - - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package -) - -const ( - errorReportContextKey = "context" - errorReportServiceContextKey = "serviceContext" -) - -// ErrorReportContextForCaller returns a structured logging field for error report context for the provided caller. -func ErrorReportContextForCaller(caller zapcore.EntryCaller) zapcore.Field { - return ErrorReportContextForSourceLocation(caller.PC, caller.File, caller.Line, caller.Defined) -} - -// ErrorReportContextForSourceLocation returns an error report context structured logging field for a source location. -func ErrorReportContextForSourceLocation(pc uintptr, file string, line int, ok bool) zapcore.Field { - if !ok { - return zap.Skip() - } - return ErrorReportContext(file, line, runtime.FuncForPC(pc).Name()) -} - -// ErrorReportContext returns a structured logging field for error report context for the provided caller. -func ErrorReportContext(file string, line int, function string) zapcore.Field { - return zap.Object(errorReportContextKey, errorReportContext{ - reportLocation: errorReportLocation{ - filePath: file, - line: line, - functionName: function, - }, - }) -} - -// ErrorReportServiceContext returns a structured logging field for error report context for the provided caller. -func ErrorReportServiceContext(serviceName, serviceVersion string) zapcore.Field { - return zap.Object(errorReportServiceContextKey, errorReportServiceContext{ - name: serviceName, - version: serviceVersion, - }) -} - -type errorReportServiceContext struct { - name string - version string -} - -func (s errorReportServiceContext) MarshalLogObject(encoder zapcore.ObjectEncoder) error { - encoder.AddString("name", s.name) - encoder.AddString("version", s.version) - return nil -} - -type errorReportContext struct { - reportLocation errorReportLocation -} - -func (c errorReportContext) MarshalLogObject(encoder zapcore.ObjectEncoder) error { - return encoder.AddObject("reportLocation", c.reportLocation) -} - -type errorReportLocation struct { - filePath string - line int - functionName string -} - -func (l errorReportLocation) MarshalLogObject(enc zapcore.ObjectEncoder) error { - enc.AddString("filePath", l.filePath) - enc.AddInt("lineNumber", l.line) - enc.AddString("functionName", l.functionName) - return nil -} diff --git a/cloudzap/httprequest.go b/cloudzap/httprequest.go deleted file mode 100644 index 33530b02..00000000 --- a/cloudzap/httprequest.go +++ /dev/null @@ -1,118 +0,0 @@ -package cloudzap - -import ( - "fmt" - "strconv" - "strings" - "time" - "unicode/utf8" - - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package -) - -// HTTPRequest creates a new zap.Field for a Cloud Logging HTTP request. -// See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest -func HTTPRequest(r *HTTPRequestObject) zap.Field { - return zap.Object("httpRequest", r) -} - -// HTTPRequestObject is a common message for logging HTTP requests. -// See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest -type HTTPRequestObject struct { - // RequestMethod is the request method. Examples: "GET", "HEAD", "PUT", "POST". - RequestMethod string - // RequestURL is the scheme (http, https), the host name, the path and the query portion of the URL - // that was requested. Example: "http://example.com/some/info?color=red". - RequestURL string - // The size of the HTTP request message in bytes, including the request headers and the request body. - RequestSize int - // Status is the response code indicating the status of response. Examples: 200, 404. - Status int - // ResponseSize is the size of the HTTP response message sent back to the client, in bytes, - // including the response headers and the response body. - ResponseSize int - // UserAgent is the user agent sent by the client. - // Example: "Mozilla/4.0 (compatible; MSIE 6.0; Windows 98; Q312461; .NET CLR 1.0.3705)". - UserAgent string - // RemoteIP is the IP address (IPv4 or IPv6) of the client that issued the HTTP request. - // This field can include port information. - // Examples: "192.168.1.1", "10.0.0.1:80", "FE80::0202:B3FF:FE1E:8329". - RemoteIP string - // ServerIP is the IP address (IPv4 or IPv6) of the origin server that the request was sent to. - // This field can include port information. - // Examples: "192.168.1.1", "10.0.0.1:80", "FE80::0202:B3FF:FE1E:8329". - ServerIP string - // Referer is the referer URL of the request, as defined in HTTP/1.1 Header Field Definitions. - Referer string - // Latency is the request processing latency on the server, from the time the request was received - // until the response was sent. - Latency time.Duration - // Protocol is the protocol used for the request. Examples: "HTTP/1.1", "HTTP/2", "websocket" - Protocol string -} - -// MarshalLogObject implements zapcore.ObjectMarshaler. -func (h *HTTPRequestObject) MarshalLogObject(encoder zapcore.ObjectEncoder) error { - if h.RequestMethod != "" { - encoder.AddString("requestMethod", h.RequestMethod) - } - if h.RequestURL != "" { - encoder.AddString("requestUrl", fixUTF8(h.RequestURL)) - } - if h.RequestSize > 0 { - addInt(encoder, "requestSize", h.RequestSize) - } - if h.Status != 0 { - encoder.AddInt("status", h.Status) - } - if h.ResponseSize > 0 { - addInt(encoder, "responseSize", h.ResponseSize) - } - if h.UserAgent != "" { - encoder.AddString("userAgent", h.UserAgent) - } - if h.RemoteIP != "" { - encoder.AddString("remoteIp", h.RemoteIP) - } - if h.ServerIP != "" { - encoder.AddString("serverIp", h.ServerIP) - } - if h.Referer != "" { - encoder.AddString("referer", h.Referer) - } - if h.Latency > 0 { - addDuration(encoder, "latency", h.Latency) - } - if h.Protocol != "" { - encoder.AddString("protocol", h.Protocol) - } - return nil -} - -func addDuration(encoder zapcore.ObjectEncoder, key string, d time.Duration) { - // A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". - encoder.AddString(key, fmt.Sprintf("%fs", d.Seconds())) -} - -func addInt(encoder zapcore.ObjectEncoder, key string, i int) { - encoder.AddString(key, strconv.Itoa(i)) -} - -// fixUTF8 is copied from cloud.google.com/logging/internal and fixes invalid UTF-8 strings. -// See: https://github.com/googleapis/google-cloud-go/issues/1383 -func fixUTF8(s string) string { - if utf8.ValidString(s) { - return s - } - var buf strings.Builder - buf.Grow(len(s)) - for _, r := range s { - if utf8.ValidRune(r) { - buf.WriteRune(r) - } else { - buf.WriteRune('\uFFFD') - } - } - return buf.String() -} diff --git a/cloudzap/level.go b/cloudzap/level.go deleted file mode 100644 index 5615ec8b..00000000 --- a/cloudzap/level.go +++ /dev/null @@ -1,50 +0,0 @@ -package cloudzap - -import ( - "log/slog" - - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package -) - -// LevelToSeverity converts a zapcore.Level to its corresponding Cloud Logging severity level. -// See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity. -// -// Deprecated: Use slog.Level directly with cloudslog.NewHandler, which maps levels automatically. -func LevelToSeverity(l zapcore.Level) string { - switch l { - case zapcore.DebugLevel: - return "DEBUG" - case zapcore.InfoLevel: - return "INFO" - case zapcore.WarnLevel: - return "WARNING" - case zapcore.ErrorLevel: - return "ERROR" - case zapcore.DPanicLevel: - return "CRITICAL" - case zapcore.PanicLevel: - return "ALERT" - case zapcore.FatalLevel: - return "EMERGENCY" - default: - return "DEFAULT" - } -} - -// LevelToSlog converts a [zapcore.Level] to a [slog.Level]. -// -// Deprecated: Use slog.Level directly with cloudslog.LoggerConfig. -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 - } -} diff --git a/cloudzap/logger.go b/cloudzap/logger.go deleted file mode 100644 index 5655240b..00000000 --- a/cloudzap/logger.go +++ /dev/null @@ -1,146 +0,0 @@ -package cloudzap - -import ( - "fmt" - - "go.einride.tech/cloudrunner/cloudruntime" - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package -) - -// LoggerConfig configures the application logger. -// -// Deprecated: Use cloudslog.LoggerConfig instead. -type LoggerConfig struct { - // Development indicates if the logger should output human-readable output for development. - Development bool `default:"true" onGCE:"false"` - // Level indicates which log level the logger should output at. - Level zapcore.Level `default:"debug" onGCE:"info"` - // ReportErrors indicates if error reports should be logged for errors. - ReportErrors bool `onGCE:"true"` -} - -// NewLogger creates a new Logger. -// -// Deprecated: Use cloudslog.NewHandler instead. -func NewLogger(config LoggerConfig) (*zap.Logger, error) { - if config.Development { - zapConfig := zap.NewDevelopmentConfig() - zapConfig.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder - zapConfig.Level = zap.NewAtomicLevelAt(config.Level) - return zapConfig.Build( - zap.AddCaller(), - zap.AddStacktrace(zap.FatalLevel), // add stacktraces manually where needed - ) - } - zapConfig := zap.NewProductionConfig() - zapConfig.EncoderConfig = NewEncoderConfig() - zapConfig.Level = zap.NewAtomicLevelAt(config.Level) - zapOptions := []zap.Option{ - zap.AddCaller(), - zap.AddStacktrace(zap.FatalLevel), // add stacktraces manually where needed - zap.WrapCore(func(core zapcore.Core) zapcore.Core { - return sourceLocationCore{nextCore: core} - }), - } - if config.ReportErrors { - if service, ok := cloudruntime.Service(); ok { - if serviceVersion, ok := cloudruntime.ServiceVersion(); ok { - zapOptions = append(zapOptions, zap.WrapCore(func(core zapcore.Core) zapcore.Core { - return errorReportingCore{ - nextCore: core, - serviceName: service, - serviceVersion: serviceVersion, - } - })) - } - } - } - logger, err := zapConfig.Build(zapOptions...) - if err != nil { - return nil, fmt.Errorf("init logger: %w", err) - } - return logger, nil -} - -type sourceLocationCore struct { - nextCore zapcore.Core -} - -func (c sourceLocationCore) Enabled(level zapcore.Level) bool { - return c.nextCore.Enabled(level) -} - -func (c sourceLocationCore) With(fields []zapcore.Field) zapcore.Core { - return sourceLocationCore{ - nextCore: c.nextCore.With(fields), - } -} - -func (c sourceLocationCore) Sync() error { - return c.nextCore.Sync() -} - -// Check implements zapcore.Core. -func (c sourceLocationCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { - if !c.nextCore.Enabled(entry.Level) { - return checked - } - return checked.AddCore(entry, c) -} - -// Write implements zapcore.Core. -func (c sourceLocationCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { - if entry.Caller.Defined { - fields = appendIfNotExists(fields, SourceLocationForCaller(entry.Caller)) - } - return c.nextCore.Write(entry, fields) -} - -type errorReportingCore struct { - nextCore zapcore.Core - serviceName string - serviceVersion string -} - -func (c errorReportingCore) Enabled(level zapcore.Level) bool { - return c.nextCore.Enabled(level) -} - -func (c errorReportingCore) With(fields []zapcore.Field) zapcore.Core { - return errorReportingCore{ - nextCore: c.nextCore.With(fields), - serviceName: c.serviceName, - serviceVersion: c.serviceVersion, - } -} - -func (c errorReportingCore) Sync() error { - return c.nextCore.Sync() -} - -// Check implements zapcore.Core. -func (c errorReportingCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { - if !c.nextCore.Enabled(entry.Level) { - return checked - } - return checked.AddCore(entry, c) -} - -// Write implements zapcore.Core. -func (c errorReportingCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { - if entry.Caller.Defined && zap.ErrorLevel.Enabled(entry.Level) { - fields = appendIfNotExists(fields, ErrorReportContextForCaller(entry.Caller)) - fields = appendIfNotExists(fields, ErrorReportServiceContext(c.serviceName, c.serviceVersion)) - } - return c.nextCore.Write(entry, fields) -} - -func appendIfNotExists(fields []zapcore.Field, field zap.Field) []zapcore.Field { - for _, existing := range fields { - if existing.Key == field.Key { - return fields - } - } - return append(fields, field) -} diff --git a/cloudzap/middleware.go b/cloudzap/middleware.go deleted file mode 100644 index 746d1038..00000000 --- a/cloudzap/middleware.go +++ /dev/null @@ -1,45 +0,0 @@ -package cloudzap - -import ( - "context" - "net/http" - - "go.einride.tech/cloudrunner/cloudstream" - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "google.golang.org/grpc" -) - -// Middleware injects a zap logger into the request context. -// -// Deprecated: The default slog logger configured by cloudrunner.Run handles logging -// without requiring middleware. Use slog.InfoContext, slog.WarnContext, etc. instead. -type Middleware struct { - Logger *zap.Logger -} - -// HTTPServer implements HTTP server middleware to add a logger to the request context. -func (l *Middleware) HTTPServer(next http.Handler) http.Handler { - return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - next.ServeHTTP(writer, request.WithContext(WithLogger(request.Context(), l.Logger))) - }) -} - -// GRPCUnaryServerInterceptor implements grpc.UnaryServerInterceptor to add a logger to the request context. -func (l *Middleware) GRPCUnaryServerInterceptor( - ctx context.Context, - request interface{}, - _ *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, -) (interface{}, error) { - return handler(WithLogger(ctx, l.Logger), request) -} - -// GRPCStreamServerInterceptor adds a zap logger to the server stream context. -func (l *Middleware) GRPCStreamServerInterceptor( - srv interface{}, - ss grpc.ServerStream, - _ *grpc.StreamServerInfo, - handler grpc.StreamHandler, -) (err error) { - return handler(srv, cloudstream.NewContextualServerStream(WithLogger(ss.Context(), l.Logger), ss)) -} diff --git a/cloudzap/proto.go b/cloudzap/proto.go deleted file mode 100644 index 1f85f0cd..00000000 --- a/cloudzap/proto.go +++ /dev/null @@ -1,24 +0,0 @@ -package cloudzap - -import ( - "encoding/json" - - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" -) - -// ProtoMessage constructs a zap.Field with the given key and proto message encoded as JSON. -func ProtoMessage(key string, message proto.Message) zap.Field { - return zap.Reflect(key, reflectProtoMessage{message: message}) -} - -type reflectProtoMessage struct { - message proto.Message -} - -var _ json.Marshaler = reflectProtoMessage{} - -func (p reflectProtoMessage) MarshalJSON() ([]byte, error) { - return protojson.Marshal(p.message) -} diff --git a/cloudzap/proto_test.go b/cloudzap/proto_test.go deleted file mode 100644 index 05d5bb0a..00000000 --- a/cloudzap/proto_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package cloudzap - -import ( - "strings" - "testing" - - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zaptest" //nolint:gomodguard // cloudzap is a zap integration package - "google.golang.org/genproto/googleapis/example/library/v1" - "gotest.tools/v3/assert" -) - -func TestProtoMessage(t *testing.T) { - var buffer zaptest.Buffer - encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - logger := zap.New(zapcore.NewCore(encoder, &buffer, zap.DebugLevel)) - logger.Info("test", ProtoMessage("protoMessage", &library.Book{ - Name: "name", - Author: "author", - Title: "title", - Read: true, - })) - assert.Assert( - t, - strings.Contains( - buffer.Stripped(), - `"protoMessage":{"name":"name","author":"author","title":"title","read":true}`, - ), - ) -} diff --git a/cloudzap/resource.go b/cloudzap/resource.go deleted file mode 100644 index efbe581a..00000000 --- a/cloudzap/resource.go +++ /dev/null @@ -1,28 +0,0 @@ -package cloudzap - -import ( - "go.opentelemetry.io/otel/sdk/resource" - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package -) - -// Resource constructs a zap.Field with the given key and OpenTelemetry resource. -func Resource(key string, r *resource.Resource) zap.Field { - return zap.Object(key, resourceObjectMarshaler{resource: r}) -} - -type resourceObjectMarshaler struct { - resource *resource.Resource -} - -// MarshalLogObject implements zapcore.ObjectMarshaler. -func (r resourceObjectMarshaler) MarshalLogObject(encoder zapcore.ObjectEncoder) error { - it := r.resource.Iter() - for it.Next() { - attr := it.Attribute() - if err := encoder.AddReflected(string(attr.Key), attr.Value.AsInterface()); err != nil { - return err - } - } - return nil -} diff --git a/cloudzap/resource_test.go b/cloudzap/resource_test.go deleted file mode 100644 index e65d7709..00000000 --- a/cloudzap/resource_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package cloudzap - -import ( - "context" - "strings" - "testing" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/sdk/resource" - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zaptest" //nolint:gomodguard // cloudzap is a zap integration package - "gotest.tools/v3/assert" -) - -func TestResource(t *testing.T) { - var buffer zaptest.Buffer - encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - input, err := resource.New(context.Background(), resource.WithAttributes( - attribute.KeyValue{ - Key: "foo", - Value: attribute.StringValue("bar"), - }, - )) - assert.NilError(t, err) - logger := zap.New(zapcore.NewCore(encoder, &buffer, zap.DebugLevel)) - logger.Info("test", Resource("resource", input)) - assert.Assert( - t, - strings.Contains( - buffer.Stripped(), - `"resource":{"foo":"bar"}`, - ), - ) -} diff --git a/cloudzap/sourcelocation.go b/cloudzap/sourcelocation.go deleted file mode 100644 index 1efbe5f4..00000000 --- a/cloudzap/sourcelocation.go +++ /dev/null @@ -1,41 +0,0 @@ -package cloudzap - -import ( - "runtime" - "strconv" - - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package -) - -const sourceLocationKey = "logging.googleapis.com/sourceLocation" - -// SourceLocationForCaller returns a structured logging field for the source location of the provided caller. -func SourceLocationForCaller(caller zapcore.EntryCaller) zapcore.Field { - return SourceLocation(caller.PC, caller.File, caller.Line, caller.Defined) -} - -// SourceLocation returns a structured logging field for the provided source location. -func SourceLocation(pc uintptr, file string, line int, ok bool) zapcore.Field { - if !ok { - return zap.Skip() - } - return zap.Object(sourceLocationKey, sourceLocation{ - file: file, - line: line, - function: runtime.FuncForPC(pc).Name(), - }) -} - -type sourceLocation struct { - file string - line int - function string -} - -func (s sourceLocation) MarshalLogObject(encoder zapcore.ObjectEncoder) error { - encoder.AddString("file", s.file) - encoder.AddString("line", strconv.Itoa(s.line)) - encoder.AddString("function", s.function) - return nil -} diff --git a/cloudzap/trace.go b/cloudzap/trace.go deleted file mode 100644 index 2bc34331..00000000 --- a/cloudzap/trace.go +++ /dev/null @@ -1,35 +0,0 @@ -package cloudzap - -import ( - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package -) - -const ( - traceKey = "logging.googleapis.com/trace" - spanIDKey = "logging.googleapis.com/spanId" - traceSampledKey = "logging.googleapis.com/trace_sampled" -) - -// Trace creates a zap field for the Cloud Logging trace field. -// -// Deprecated: Use log/slog with the cloudslog package instead. The cloudslog.Handler -// automatically injects trace fields from the OpenTelemetry span context. -func Trace(traceID string) zap.Field { - return zap.String(traceKey, traceID) -} - -// SpanID creates a zap field for the Cloud Logging span ID field. -// -// Deprecated: Use log/slog with the cloudslog package instead. The cloudslog.Handler -// automatically injects span ID from the OpenTelemetry span context. -func SpanID(spanID string) zap.Field { - return zap.String(spanIDKey, spanID) -} - -// TraceSampled creates a zap field for the Cloud Logging trace sampled field. -// -// Deprecated: Use log/slog with the cloudslog package instead. The cloudslog.Handler -// automatically injects trace sampled from the OpenTelemetry span context. -func TraceSampled(sampled bool) zap.Field { - return zap.Bool(traceSampledKey, sampled) -} diff --git a/cloudzap/trace_test.go b/cloudzap/trace_test.go deleted file mode 100644 index b9d81bae..00000000 --- a/cloudzap/trace_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package cloudzap - -import ( - "strings" - "testing" - - "go.uber.org/zap" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zapcore" //nolint:gomodguard // cloudzap is a zap integration package - "go.uber.org/zap/zaptest" //nolint:gomodguard // cloudzap is a zap integration package - "gotest.tools/v3/assert" -) - -func TestTrace(t *testing.T) { - var buffer zaptest.Buffer - encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - logger := zap.New(zapcore.NewCore(encoder, &buffer, zap.DebugLevel)) - logger.Info("test", Trace("bar")) - assert.Assert( - t, - strings.Contains( - buffer.Stripped(), - `"logging.googleapis.com/trace":"bar"`, - ), - ) -} - -func TestSpanID(t *testing.T) { - var buffer zaptest.Buffer - encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - logger := zap.New(zapcore.NewCore(encoder, &buffer, zap.DebugLevel)) - logger.Info("test", SpanID("foo")) - assert.Assert( - t, - strings.Contains( - buffer.Stripped(), - `"logging.googleapis.com/spanId":"foo"`, - ), - ) -} - -func TestTraceSampled(t *testing.T) { - var buffer zaptest.Buffer - encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - logger := zap.New(zapcore.NewCore(encoder, &buffer, zap.DebugLevel)) - logger.Info("test", TraceSampled(true)) - assert.Assert( - t, - strings.Contains( - buffer.Stripped(), - `"logging.googleapis.com/trace_sampled":true`, - ), - ) -} diff --git a/go.mod b/go.mod index bf377bbb..ca759a4f 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 - go.uber.org/zap v1.27.1 golang.org/x/net v0.52.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 @@ -69,7 +68,6 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index ada1bd98..653169b3 100644 --- a/go.sum +++ b/go.sum @@ -204,11 +204,7 @@ go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/grpcserver.go b/grpcserver.go index 7002281a..96aaf007 100644 --- a/grpcserver.go +++ b/grpcserver.go @@ -28,13 +28,11 @@ func NewGRPCServer(ctx context.Context, opts ...grpc.ServerOption) *grpc.Server serverOptions = append(serverOptions, grpc.StatsHandler(otelgrpc.NewServerHandler()), grpc.ChainUnaryInterceptor( - run.loggerMiddleware.GRPCUnaryServerInterceptor, // adds context logger - unaryTracing, // needs the context logger + unaryTracing, run.requestLoggerMiddleware.GRPCUnaryServerInterceptor, // needs to run after trace run.serverMiddleware.GRPCUnaryServerInterceptor, // needs to run after request logger ), grpc.ChainStreamInterceptor( - run.loggerMiddleware.GRPCStreamServerInterceptor, streamTracing, run.requestLoggerMiddleware.GRPCStreamServerInterceptor, run.serverMiddleware.GRPCStreamServerInterceptor, diff --git a/httpserver.go b/httpserver.go index ebb75cdd..e5b472ac 100644 --- a/httpserver.go +++ b/httpserver.go @@ -39,7 +39,6 @@ func NewHTTPServer(ctx context.Context, handler http.Handler, middlewares ...HTT otelhttp.WithSpanNameFormatter(httpSpanName), ) }, - run.loggerMiddleware.HTTPServer, tracingMiddleware, run.requestLoggerMiddleware.HTTPServer, run.securityHeadersMiddleware.HTTPServer, diff --git a/logger.go b/logger.go index 571ee7cf..d5c2bb0e 100644 --- a/logger.go +++ b/logger.go @@ -4,32 +4,12 @@ import ( "context" "go.einride.tech/cloudrunner/cloudrequestlog" - "go.einride.tech/cloudrunner/cloudzap" //nolint:staticcheck // SA1019: internal use of deprecated package pending removal - "go.uber.org/zap" //nolint:gomodguard // legacy zap dependency for backwards compatibility + "go.einride.tech/cloudrunner/cloudslog" ) -// Logger returns the logger for the current context. -// -// Deprecated: Use slog.InfoContext, slog.WarnContext, slog.ErrorContext, etc. instead. -// The default slog logger is configured with cloudslog.Handler which automatically -// handles trace correlation and Cloud Logging field formatting. -func Logger(ctx context.Context) *zap.Logger { - logger, ok := cloudzap.GetLogger(ctx) - if !ok { - panic("cloudrunner.Logger must be called with a context from cloudrunner.Run") - } - return logger -} - -// WithLoggerFields attaches structured fields to a new logger in the returned child context. -// -// Deprecated: Use cloudslog.With to attach slog attributes to the context instead. -func WithLoggerFields(ctx context.Context, fields ...zap.Field) context.Context { - logger, ok := cloudzap.GetLogger(ctx) - if !ok { - panic("cloudrunner.WithLoggerFields must be called with a context from cloudrunner.Run") - } - return cloudzap.WithLogger(ctx, logger.With(fields...)) +// WithLoggerFields attaches structured fields to the returned child context. +func WithLoggerFields(ctx context.Context, args ...any) context.Context { + return cloudslog.With(ctx, args...) } // AddRequestLogFields adds fields to the current request log, and is safe to call concurrently. diff --git a/run.go b/run.go index 236bfed4..0a8bb73e 100644 --- a/run.go +++ b/run.go @@ -20,8 +20,7 @@ import ( "go.einride.tech/cloudrunner/cloudruntime" "go.einride.tech/cloudrunner/cloudserver" "go.einride.tech/cloudrunner/cloudslog" - "go.einride.tech/cloudrunner/cloudtrace" //nolint:staticcheck // SA1019: internal use of deprecated package pending removal - "go.einride.tech/cloudrunner/cloudzap" //nolint:staticcheck // SA1019: internal use of deprecated package pending removal + "go.einride.tech/cloudrunner/cloudtrace" //nolint:staticcheck // SA1019: deprecated, pending removal "google.golang.org/grpc" ) @@ -30,7 +29,7 @@ type runConfig struct { // Runtime contains runtime config. Runtime cloudruntime.Config // Logger contains logger config. - Logger cloudzap.LoggerConfig //nolint:staticcheck // SA1019: deprecated, pending removal + Logger cloudslog.LoggerConfig // Profiler contains profiler config. Profiler cloudprofiler.Config // TraceExporter contains trace exporter config. @@ -101,17 +100,11 @@ func Run(fn func(context.Context) error, options ...Option) (err error) { run.requestLoggerMiddleware.Config = run.config.RequestLogger ctx = withRunContext(ctx, &run) ctx = cloudruntime.WithConfig(ctx, run.config.Runtime) - logger, err := cloudzap.NewLogger(run.config.Logger) //nolint:staticcheck // SA1019: deprecated, pending removal - if err != nil { - return fmt.Errorf("cloudrunner.Run: %w", err) - } - run.loggerMiddleware.Logger = logger - ctx = cloudzap.WithLogger(ctx, logger) //nolint:staticcheck // SA1019: deprecated, pending removal // Set the global default log/slog logger. slog.SetDefault(slog.New(cloudslog.NewHandler(cloudslog.LoggerConfig{ ProjectID: run.config.Runtime.ProjectID, Development: run.config.Logger.Development, - Level: cloudzap.LevelToSlog(run.config.Logger.Level), //nolint:staticcheck // SA1019: deprecated + Level: run.config.Logger.Level, ProtoMessageSizeLimit: run.config.RequestLogger.MessageSizeLimit, ReportErrors: run.config.Logger.ReportErrors, }))) @@ -182,7 +175,6 @@ type runContext struct { config runConfig configOptions []cloudconfig.Option grpcServerOptions []grpc.ServerOption - loggerMiddleware cloudzap.Middleware //nolint:staticcheck // SA1019: deprecated, pending removal serverMiddleware cloudserver.Middleware clientMiddleware cloudclient.Middleware requestLoggerMiddleware cloudrequestlog.Middleware diff --git a/run_test.go b/run_test.go index 7c34076d..b2f6165f 100644 --- a/run_test.go +++ b/run_test.go @@ -3,6 +3,7 @@ package cloudrunner_test import ( "context" "log" + "log/slog" "go.einride.tech/cloudrunner" "google.golang.org/grpc/health" @@ -11,7 +12,7 @@ import ( func ExampleRun_helloWorld() { if err := cloudrunner.Run(func(ctx context.Context) error { - cloudrunner.Logger(ctx).Info("hello world") + slog.InfoContext(ctx, "hello world") return nil }); err != nil { log.Fatal(err)