--- name: fp-go-logging description: >- Use this skill when adding logging to fp-go functional pipelines (github.com/IBM/fp-go/v2). Trigger on logging inside Pipe/Flow, Tap/TapIOK/TapReaderIOK/TapLeft used for logging, TapSLog, SLogLeft/SLogRight, entry/exit logging (LogEntryExit, LogEntryExitWithCallback), correlation IDs, adding context to errors before they are logged (MapLeft with errors.OnError), request-scoped slog loggers in context.Context (logging.WithLogger, GetLoggerFromContext), or IO.Logf/Printf in IO/IOResult/ReaderIOResult. --- # fp-go Logging Logging is a **side effect**: it must never change the value or the error flowing through a pipeline. fp-go expresses this with the `Tap*` family of operators. A `Tap` runs an effect on the current value (or error) and hands the **original** computation downstream unchanged. This skill covers three tasks: 1. [Add a simple log statement](#1-a-simple-log-statement-with-tap): `TapSLog`, `TapIOK`, `TapReaderIOK`, `TapLeft` 2. [Add entry and exit logs](#2-entry-and-exit-logs): `LogEntryExit` 3. [Add context to the error channel](#3-adding-context-to-the-error-channel): `MapLeft` + `errors.OnError`, then log once Examples assume these imports; all live under `github.com/IBM/fp-go/v2`: ```go import ( "log/slog" RIO "github.com/IBM/fp-go/v2/context/readerioresult" ER "github.com/IBM/fp-go/v2/errors" F "github.com/IBM/fp-go/v2/function" IO "github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/logging" R "github.com/IBM/fp-go/v2/result" ) ``` Runnable versions of the snippets are in `v2/context/readerioresult/logging_example_test.go` (`ExampleTapSLog`, `ExampleTapIOK_logging`, `ExampleTapReaderIOK_logging`, `ExampleSLogRight`, `ExampleSLogLeft`, `ExampleLogEntryExit`, `ExampleLogEntryExit_nested`, `ExampleLogEntryExitWithCallback`, `ExampleMapLeft`, `ExampleMapLeft_logging`). ## Before You Generate fp-go is low-frequency in training data, so signatures are easy to misremember. For any combinator not shown below, look it up via the fp-go MCP server's `search_examples` / `get_example` tools (see the **fp-go-mcp** skill) instead of guessing. After writing code, run `go build ./...` and `go vet ./...` and fix any type-parameter or argument-order errors before presenting it. ## Quick Reference | Goal | Operator (in `F.Pipe` on a `ReaderIOResult[A]`) | |---|---| | Log value **or** error, structured | `RIO.TapSLog[A]("msg")` | | Same at DEBUG level | `RIO.TapSLogDebug[A]("msg")` | | Log success only | `RIO.Tap(RIO.SLogRight[A]("msg"))` | | Log error only (ERROR level) | `RIO.TapLeft[A](RIO.SLogLeft("msg"))` | | printf-style log of the value | `RIO.TapIOK(IO.Logf[A]("msg: %v"))` | | Custom attributes / custom message | `RIO.TapReaderIOK(logX)` with `logX: func(A) ReaderIO[Void]` | | Entry + exit logs with timing and correlation ID | `RIO.LogEntryExit[A]("name")` | | Entry + exit at another level | `RIO.LogEntryExitWithCallback[A](slog.LevelDebug, logging.GetLoggerFromContext, "name")` | | Add context to an error | `RIO.MapLeft[A](ER.OnError("loading user %d", id))` | | Choose the logger for a pipeline | `RIO.Local[A](logging.WithLogger(l))` | `Tap*` and `ChainFirst*` are aliases (`TapIOK` = `ChainFirstIOK`, `TapLeft` = `ChainFirstLeft`, …). Prefer the `Tap` names for logging; they state the intent. ## 1. A Simple Log Statement with `Tap` ### Default: `TapSLog` `TapSLog[A](msg)` is an `Operator[A, A]`. Put it in the pipe **after** the step you want to observe. It logs `value=` on success or `error=` on failure at INFO level, using the logger from the context, and returns the computation unchanged. ```go func greeting(id int) RIO.ReaderIOResult[string] { return F.Pipe2( fetchUser(id), RIO.TapSLog[User]("fetched user"), RIO.Map(getName), ) } // level=INFO msg="fetched user" value="{ID:42 Name:Alice}" // level=INFO msg="fetched user" error="not found" ``` - Logs happen when the pipeline **runs**, not when it is built. - The type parameter is the type at that point in the pipe (`User` here, not `string`). - `TapSLogInfo` / `TapSLogDebug` choose the level explicitly. Debug logs cost nothing when the logger's level filters them out. - `context/readerresult` and `context/readerio` have their own `TapSLog[A]` with the same usage. ### Printf-Style: `TapIOK` with `IO.Logf` Any `io.Kleisli[A, B]` (`func(A) IO[B]`) can be tapped with `TapIOK`. The `io` package has ready-made ones: ```go RIO.TapIOK(IO.Logf[User]("fetched user %+v")) // log.Printf RIO.TapIOK(IO.LogGo[User]("user {{.Name}}")) // text/template, log.Println RIO.TapIOK(IO.Printf[User]("fetched user %+v\n")) // fmt.Printf (stdout, no timestamp) RIO.TapIOK(IO.Logger[User](myStdLogger)("user")) // custom *log.Logger ``` These use the standard `log` package, not `slog`, and log **only on success**; an error skips the tap. Prefer `TapSLog` when the application uses structured logging. The same operator exists for the other monads: `IOR.TapIOK` (ioresult), `RR.TapIOK` (context/readerresult), `IO.ChainFirst` (plain IO). ### Custom Attributes: `TapReaderIOK` When you want a specific message and specific attributes, write a small named `func(A) ReaderIO[Void]` that reads the logger from the context. Reading from the context means the log uses the request-scoped logger and inherits the correlation ID of an enclosing `LogEntryExit`. ```go import RIOC "github.com/IBM/fp-go/v2/context/readerio" func logUserLoaded(u User) RIOC.ReaderIO[F.Void] { return func(ctx context.Context) IO.IO[F.Void] { return func() F.Void { logging.GetLoggerFromContext(ctx).InfoContext(ctx, "user loaded", "id", u.ID, "name", u.Name) return F.VOID } } } pipeline := F.Pipe2( fetchUser(42), RIO.TapReaderIOK(logUserLoaded), RIO.Map(getName), ) // level=INFO msg="user loaded" id=42 name=Alice ``` Do not call `slog.Info(...)` from inside `RIO.Map`: `Map` is for pure functions, and the global logger ignores the logger scoped to the context. ### Success Only / Error Only `SLogRight` and `SLogLeft` are Kleisli arrows, so they go through `Tap` / `TapLeft`: ```go RIO.Tap(RIO.SLogRight[User]("fetched user")) // INFO, success only RIO.TapLeft[User](RIO.SLogLeft("fetching user failed")) // ERROR, error only; the error still propagates ``` `TapLeft` needs the explicit `[A]` because `A` cannot be inferred from the error handler. ### Common Mistakes ```go // ❌ SLog is Kleisli[Result[A], Void]: its input is the Result, not A, so this does not compile RIO.Chain(RIO.SLog[User]("fetched")) // ✅ RIO.TapSLog[User]("fetched") // ❌ Chain replaces the value with Void RIO.Chain(RIO.SLogRight[User]("fetched")) // ✅ RIO.Tap(RIO.SLogRight[User]("fetched")) // ❌ side effect inside a pure Map; bypasses the context logger RIO.Map(func(u User) User { slog.Info("fetched", "user", u); return u }) // ✅ RIO.TapSLog[User]("fetched") ``` ## 2. Entry and Exit Logs `LogEntryExit[A](name)` is an `Operator[A, A]` that **wraps the computation it is applied to**. It logs `[entering]` before that computation starts and `[exiting ]` (success) or `[throwing]` (failure, with the error) after it ends. Both records carry the same correlation `ID`, and the exit record carries the `duration`. ```go func loadUser(id int) RIO.ReaderIOResult[User] { return F.Pipe1( fetchUser(id), RIO.LogEntryExit[User]("loadUser"), ) } // level=INFO msg=[entering] ID=7 name=loadUser // level=INFO msg="[exiting ]" ID=7 name=loadUser duration=1.2ms // // on failure: // level=INFO msg=[entering] ID=8 name=loadUser // level=INFO msg=[throwing] ID=8 name=loadUser duration=0.4ms error="not found" ``` ### What Gets Wrapped In a `F.Pipe`, `LogEntryExit` wraps **everything above it**. Put it last to time a whole function, or apply it to a single step to time just that step: ```go // Whole pipeline: the TapSLog runs between entry and exit and is logged with the same ID loadName := F.Pipe3( fetchUser(42), RIO.TapSLog[User]("fetched user"), RIO.Map(getName), RIO.LogEntryExit[string]("loadName"), ) // level=INFO msg=[entering] ID=3 name=loadName // level=INFO msg="fetched user" ID=3 value="{ID:42 Name:Alice}" // level=INFO msg="[exiting ]" ID=3 name=loadName duration=… // One step inside a larger pipeline: wrap the Kleisli, not the pipeline F.Pipe2( fetchUser(42), RIO.Chain(F.Flow2(fetchOrders, RIO.LogEntryExit[[]Order]("fetchOrders"))), RIO.Map(summarize), ) ``` Nested `LogEntryExit` calls each get their own ID. The ID is installed on the context logger for the wrapped computation, so every context-aware log inside it (`TapSLog`, `SLogLeft`, `TapReaderIOK` using `GetLoggerFromContext`) is correlated automatically. ### Why Not `TapSLog("entering")`? A `Tap` placed before a step runs after the **previous** step. It can mark "about to start", but it cannot log the end of a failed step, measure the duration or correlate the two records. Use `LogEntryExit` for entry/exit logging. ### Level and Logger ```go RIO.LogEntryExitWithCallback[User](slog.LevelDebug, logging.GetLoggerFromContext, "loadUser") ``` If the logger does not have the level enabled, the whole instrumentation (ID, timer, both records) is skipped. The second argument is any `func(context.Context) *slog.Logger`. ## 3. Adding Context to the Error Channel An error usually surfaces far from where it happened. Logging it at every layer produces duplicate records that lack context. The idiomatic approach: 1. **Enrich** the error where the context is known, with `MapLeft` and `errors.OnError`. 2. **Log once**, at the boundary (handler, command, job), with `TapLeft(SLogLeft(...))`, `TapSLog`, or the `[throwing]` record of `LogEntryExit`. ### `MapLeft` + `errors.OnError` `RIO.MapLeft[A](f Endomorphism[error])` rewrites the error and leaves successes untouched. `ER.OnError(format, args...)` builds such an endomorphism. It wraps the original error with `%w`, so `errors.Is` / `errors.As` keep working. ```go func loadGreeting(id int) RIO.ReaderIOResult[string] { return F.Pipe2( fetchUser(id), RIO.Map(getName), RIO.MapLeft[string](ER.OnError("loading greeting for user %d", id)), ) } // err.Error() == "loading greeting for user 7, Caused By: not found" // errors.Is(err, ErrNotFound) == true ``` Capture the inputs that explain the failure (`id` above) from the enclosing Kleisli's parameters. The error then says *which* call failed, not just *that* one failed. ### Log Once at the Boundary ```go func handle(id int) RIO.ReaderIOResult[string] { return F.Pipe2( loadGreeting(id), RIO.MapLeft[string](ER.OnError("handling request")), RIO.TapLeft[string](RIO.SLogLeft("request failed")), ) } // level=ERROR msg="request failed" error="handling request, Caused By: loading greeting for user 7, Caused By: not found" ``` ### Combine with `LogEntryExit` `[throwing]` logs the error as it leaves the wrapped computation. Put `MapLeft` **above** `LogEntryExit` so the exit record includes the context: ```go F.Pipe2( fetchUser(id), RIO.MapLeft[User](ER.OnError("loading user %d", id)), // enrich first … RIO.LogEntryExit[User]("loadUser"), // … then [throwing] logs the enriched error ) ``` ### Rules - Use `MapLeft` to change the error. Do not use `OrElse`/`ChainLeft` with `RIO.Left` just to rewrap; those are for recovery. - Always wrap with `%w` (which `ER.OnError` does). `fmt.Errorf("…: %v", err)` breaks `errors.Is`. - Enrich where the context is; log where the error is handled. A `TapLeft` log in every layer duplicates the record. - `MapLeft` is available in `context/readerioresult`, `context/readerresult`, `context/readerreaderioresult`, `readerresult`, `result` and `ioresult`. In `result`/`ioresult` the function has type `func(error) E`; `ER.OnError(...)` fits with `E = error`. ## Choosing the Logger All context-aware operators (`TapSLog*`, `SLogLeft/Right`, `LogEntryExit*`) read the logger with `logging.GetLoggerFromContext`, which falls back to the global logger. ```go // Global logger (defaults to slog.Default()) old := logging.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil))) defer logging.SetLogger(old) // Request-scoped logger: install it around the whole pipeline with Local. // logging.WithLogger(l) is the ctx → Pair[CancelFunc, ctx] arrow that Local expects. reqLogger := logging.GetLogger().With("requestID", requestID) value, err := R.Unwrap(F.Pipe1( pipeline, RIO.Local[Data](logging.WithLogger(reqLogger)), )(ctx)()) ``` `Local` exists for `context/readerio`, `context/readerresult` and `context/statereaderioresult` as well. To get a plain enriched context outside a pipeline, use `_, ctx2 := pair.Unpack(logging.WithLogger(reqLogger)(ctx))`. For a logger stored under your own key, build the callback with `F.Flow2(CR.AskValue[*slog.Logger](myKey), O.GetOrElse(slog.Default))`. For general context handling see the `fp-go-context` skill. ## Package Overview | Package | Logging API | |---|---| | `context/readerioresult` | `TapSLog`, `TapSLogInfo`, `TapSLogDebug`, `SLog*`, `SLogLeft`, `SLogRight`, `SLogWithCallback`, `LogEntryExit`, `LogEntryExitWithCallback`, `MapLeft`, `Tap*`, `Local` | | `context/readerresult` | `TapSLog`, `SLog` (`Kleisli[Result[A], A]`), `TapIOK`, `MapLeft`, `Local` | | `context/readerio` | `TapSLog`, `SLog`, `Tap`, `TapIOK`, `Local` | | `io` | `Logf`, `LogGo`, `Printf`, `PrintGo`, `Logger`: `func(A) IO[A]`, used with `TapIOK` | | `readerio` | `Logf`, `LogGo`, `Printf`, `PrintGo` for the generic ReaderIO | | `logging` | `GetLogger`, `SetLogger`, `WithLogger`, `GetLoggerFromContext`, `LoggingCallbacks` | | `errors` | `OnError` (wrap with context), `OnNone`, `OnSome` (build errors) | `SLogWithCallback[A](level, cb, msg)` has the shape of `SLog`. Turn it into an operator with `RIOC.Tap(...)` (`context/readerio`) when you need `TapSLog` with a custom level and logger. ## Complete Example ```go func loadUser(id int) RIO.ReaderIOResult[User] { return F.Pipe3( fetchUser(id), RIO.TapSLogDebug[User]("fetched user"), // simple log statement RIO.MapLeft[User](ER.OnError("loading user %d", id)), // add context to the error RIO.LogEntryExit[User]("loadUser"), // entry/exit around the three steps ) } func handleGreeting(reqLogger *slog.Logger, id int) RIO.ReaderIOResult[string] { return F.Pipe3( loadUser(id), RIO.Map(getName), RIO.TapLeft[string](RIO.SLogLeft("greeting failed")), // log the error once, at the boundary RIO.Local[string](logging.WithLogger(reqLogger)), // every log above uses reqLogger ) } name, err := R.Unwrap(handleGreeting(reqLogger, 7)(ctx)()) ```