mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-17 23:37:41 +02:00
216 lines
7.3 KiB
Go
216 lines
7.3 KiB
Go
// Copyright (c) 2023 - 2025 IBM Corp.
|
|
// All rights reserved.
|
|
//
|
|
// 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 readerresult provides logging utilities for the ReaderResult monad,
|
|
// which combines the Reader monad (for dependency injection via context.Context)
|
|
// with the Result monad (for error handling).
|
|
//
|
|
// The logging functions in this package allow you to log Result values (both
|
|
// successes and errors) while preserving the functional composition style.
|
|
package readerresult
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
|
|
F "github.com/IBM/fp-go/v2/function"
|
|
"github.com/IBM/fp-go/v2/logging"
|
|
"github.com/IBM/fp-go/v2/reader"
|
|
"github.com/IBM/fp-go/v2/result"
|
|
)
|
|
|
|
// curriedLog creates a curried logging function that takes an slog.Attr and a context,
|
|
// then logs the attribute with the specified log level and message.
|
|
//
|
|
// This is an internal helper function used to create the logging pipeline in a
|
|
// point-free style. The currying allows for partial application in functional
|
|
// composition.
|
|
//
|
|
// Parameters:
|
|
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelError)
|
|
// - cb: A callback function that retrieves a logger from the context
|
|
// - message: The log message to display
|
|
//
|
|
// Returns:
|
|
// - A curried function that takes an slog.Attr, then a context, and performs logging
|
|
func curriedLog(
|
|
logLevel slog.Level,
|
|
cb func(context.Context) *slog.Logger,
|
|
message string) func(slog.Attr) Reader[context.Context, struct{}] {
|
|
return F.Curry2(func(a slog.Attr, ctx context.Context) struct{} {
|
|
cb(ctx).LogAttrs(ctx, logLevel, message, a)
|
|
return struct{}{}
|
|
})
|
|
}
|
|
|
|
// SLogWithCallback creates a Kleisli arrow that logs a Result value using a custom
|
|
// logger callback and log level. The Result value is logged and then returned unchanged,
|
|
// making this function suitable for use in functional pipelines.
|
|
//
|
|
// This function logs both successful values and errors:
|
|
// - Success values are logged with the key "value"
|
|
// - Error values are logged with the key "error"
|
|
//
|
|
// The logging is performed as a side effect while preserving the Result value,
|
|
// allowing it to be used in the middle of a computation pipeline without
|
|
// interrupting the flow.
|
|
//
|
|
// Type Parameters:
|
|
// - A: The type of the success value in the Result
|
|
//
|
|
// Parameters:
|
|
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelDebug, LevelError)
|
|
// - cb: A callback function that retrieves a *slog.Logger from the context
|
|
// - message: The log message to display
|
|
//
|
|
// Returns:
|
|
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
|
// The returned ReaderResult, when executed with a context, logs the Result
|
|
// and returns it unchanged
|
|
//
|
|
// Example:
|
|
//
|
|
// type User struct {
|
|
// ID int
|
|
// Name string
|
|
// }
|
|
//
|
|
// // Custom logger callback
|
|
// getLogger := func(ctx context.Context) *slog.Logger {
|
|
// return slog.Default()
|
|
// }
|
|
//
|
|
// // Create a logging function for debug level
|
|
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
|
|
//
|
|
// // Use in a pipeline
|
|
// ctx := context.Background()
|
|
// user := result.Of(User{ID: 123, Name: "Alice"})
|
|
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
|
|
// // logged still contains the User value
|
|
//
|
|
// Example with error:
|
|
//
|
|
// err := errors.New("user not found")
|
|
// userResult := result.Left[User](err)
|
|
// logged := logDebug(userResult)(ctx) // Logs: level=DEBUG msg="User data" error="user not found"
|
|
// // logged still contains the error
|
|
func SLogWithCallback[A any](
|
|
logLevel slog.Level,
|
|
cb func(context.Context) *slog.Logger,
|
|
message string) Kleisli[Result[A], A] {
|
|
|
|
return F.Pipe1(
|
|
F.Flow2(
|
|
result.ToSLogAttr[A](),
|
|
curriedLog(logLevel, cb, message),
|
|
),
|
|
reader.Chain(reader.Sequence(F.Flow2( // this flow is basically the `MapTo` function with side effects
|
|
reader.Of[struct{}, Result[A]],
|
|
reader.Map[context.Context, struct{}, Result[A]],
|
|
))),
|
|
)
|
|
|
|
}
|
|
|
|
// SLog creates a Kleisli arrow that logs a Result value at INFO level using the
|
|
// logger from the context. This is a convenience function that uses SLogWithCallback
|
|
// with default settings.
|
|
//
|
|
// The Result value is logged and then returned unchanged, making this function
|
|
// suitable for use in functional pipelines for debugging or monitoring purposes.
|
|
//
|
|
// This function logs both successful values and errors:
|
|
// - Success values are logged with the key "value"
|
|
// - Error values are logged with the key "error"
|
|
//
|
|
// Type Parameters:
|
|
// - A: The type of the success value in the Result
|
|
//
|
|
// Parameters:
|
|
// - message: The log message to display
|
|
//
|
|
// Returns:
|
|
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
|
// The returned ReaderResult, when executed with a context, logs the Result
|
|
// at INFO level and returns it unchanged
|
|
//
|
|
// Example - Logging a successful computation:
|
|
//
|
|
// ctx := context.Background()
|
|
//
|
|
// // Simple value logging
|
|
// res := result.Of(42)
|
|
// logged := SLog[int]("Processing number")(res)(ctx)
|
|
// // Logs: level=INFO msg="Processing number" value=42
|
|
// // logged == result.Of(42)
|
|
//
|
|
// Example - Logging in a pipeline:
|
|
//
|
|
// type User struct {
|
|
// ID int
|
|
// Name string
|
|
// }
|
|
//
|
|
// fetchUser := func(id int) result.Result[User] {
|
|
// return result.Of(User{ID: id, Name: "Alice"})
|
|
// }
|
|
//
|
|
// processUser := func(user User) result.Result[string] {
|
|
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
|
|
// }
|
|
//
|
|
// ctx := context.Background()
|
|
//
|
|
// // Log at each step
|
|
// userResult := fetchUser(123)
|
|
// logged1 := SLog[User]("Fetched user")(userResult)(ctx)
|
|
// // Logs: level=INFO msg="Fetched user" value={ID:123 Name:Alice}
|
|
//
|
|
// processed := result.Chain(processUser)(logged1)
|
|
// logged2 := SLog[string]("Processed user")(processed)(ctx)
|
|
// // Logs: level=INFO msg="Processed user" value="Processed: Alice"
|
|
//
|
|
// Example - Logging errors:
|
|
//
|
|
// err := errors.New("database connection failed")
|
|
// errResult := result.Left[User](err)
|
|
// logged := SLog[User]("Database operation")(errResult)(ctx)
|
|
// // Logs: level=INFO msg="Database operation" error="database connection failed"
|
|
// // logged still contains the error
|
|
//
|
|
// Example - Using with context logger:
|
|
//
|
|
// // Set up a custom logger in the context
|
|
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
// ctx := logging.WithLogger(logger)(context.Background())
|
|
//
|
|
// res := result.Of("important data")
|
|
// logged := SLog[string]("Critical operation")(res)(ctx)
|
|
// // Uses the logger from context to log the message
|
|
//
|
|
// Note: The function uses logging.GetLoggerFromContext to retrieve the logger,
|
|
// which falls back to the global logger if no logger is found in the context.
|
|
//
|
|
//go:inline
|
|
func SLog[A any](message string) Kleisli[Result[A], A] {
|
|
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
|
|
}
|
|
|
|
//go:inline
|
|
func TapSLog[A any](message string) Operator[A, A] {
|
|
return reader.Chain(SLog[A](message))
|
|
}
|