ibm/fp-go · Archived

fp-go-logging

Use this skill when working with logging in fp-go functional pipelines (github.com/IBM/fp-go/v2/logging).

First seen Jun 23, 2026

Installation

$ npx skills add ibm/fp-go --skill fp-go-logging

Summary

  • Use this skill when working with logging in fp-go functional pipelines (github.com/IBM/fp-go/v2/logging).
  • Trigger on mentions of logging in monadic contexts, ChainFirstIOK, ChainFirst with logging, side-effect logging, structured logging (SLog), context-embedded loggers, LoggingCallbacks, TapSLog, LogEntryExit, Printf/Logf in IO/ReaderIO/ReaderIOResult, or when the user needs to add logging to functional pipelines without breaking the data flow.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from ibm/fp-go.

npx skills add ibm/fp-go

Browse all from ibm/fp-go

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2.0K
License LICENSE
Default branch main
Open issues 2
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 16,173 B
  • docs SUMMARY.md 470 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

fp-go Logging

Overview

fp-go provides logging utilities that integrate naturally with functional pipelines. Logging is always a side effect — it should not change the value being processed. The library achieves this through ChainFirst-style combinators that thread the original value through unchanged while performing the log.

Packages

Package Purpose
github.com/IBM/fp-go/v2/logging Global logger, context-embedded logger, LoggingCallbacks
github.com/IBM/fp-go/v2/io Logf, Logger, LogGo, Printf, PrintGo — IO-level logging helpers
github.com/IBM/fp-go/v2/readerio Logf, Printf, LogGo, PrintGo — printf/template logging for ReaderIO (no SLog here)
github.com/IBM/fp-go/v2/context/readerio SLog, TapSLog, SLogWithCallback — structured logging for context ReaderIO
github.com/IBM/fp-go/v2/context/readerresult SLog, TapSLog, SLogWithCallback — structured logging for ReaderResult
github.com/IBM/fp-go/v2/context/readerioresult TapSLog/TapSLogInfo/TapSLogDebug, SLog/SLogInfo/SLogDebug, SLogLeft, SLogRight, SLogWithCallback, LogEntryExit, LogEntryExitWithCallback — full suite for ReaderIOResult

Logging Inside Pipelines

The idiomatic way to log inside a monadic pipeline is ChainFirstIOK (or ChainFirst where the monad is already IO). These combinators execute a side-effecting function and pass the original value downstream unchanged.

With IOResult / ReaderIOResult — printf-style

import (
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    IO  "github.com/IBM/fp-go/v2/io"
    F   "github.com/IBM/fp-go/v2/function"
)

pipeline := F.Pipe3(
    fetchUser(42),
    RIO.ChainResultK(result.Eitherize1(validateUser)), // Kleisli User → Result[User]
    // Log after validation — value flows through unchanged
    RIO.ChainFirstIOK(IO.Logf[User]("Validated user: %v")),
    RIO.Map(enrichUser),
)

IO.Logf[A](format string) func(A) IO[A] logs using log.Printf and returns the value unchanged. It's a Kleisli arrow suitable for ChainFirst and ChainFirstIOK.

With IOResult / plain IO

import (
    IOR  "github.com/IBM/fp-go/v2/ioresult"
    IOF  "github.com/IBM/fp-go/v2/ioresult/file"   // ReadFile, WriteFile, …
    IO   "github.com/IBM/fp-go/v2/io"
    J    "github.com/IBM/fp-go/v2/json"
    F    "github.com/IBM/fp-go/v2/function"
)

pipeline := F.Pipe3(
    IOF.ReadFile("config.json"),                        // IOResult[[]byte]
    IOR.ChainResultK(J.Unmarshal[Config]),              // []byte → Result[Config]
    IOR.ChainFirstIOK(IO.Logf[Config]("Loaded config: %v")),
    IOR.Map(processConfig),
)

(ioeither/file and ioeither are the Either[E, A] equivalents; reach for them only when the error type is not error. IOR.Map needs no leading type parameter — IOE.Map[error] does.)

Logging Arrays in TraverseArray

import (
    A   "github.com/IBM/fp-go/v2/array"
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    IO  "github.com/IBM/fp-go/v2/io"
    F   "github.com/IBM/fp-go/v2/function"
)

// Log each item individually, then log the final slice
pipeline := F.Pipe2(
    A.MakeBy(3, idxToFilename),
    RIO.TraverseArray(F.Flow3(
        RIO.Eitherize1(readFileCtx),                  // func(ctx, string) ([]byte, error)
        RIO.ChainResultK(J.Unmarshal[Record]),
        RIO.ChainFirstIOK(IO.Logf[Record]("Parsed record: %v")),
    )),
    RIO.ChainFirstIOK(IO.Logf[[]Record]("All records: %v")),
)

IO Logging Functions

All live in github.com/IBM/fp-go/v2/io:

Logf — printf-style

IO.Logf[A any](format string) func(A) IO[A]

Uses log.Printf. The format string works like fmt.Sprintf.

IO.Logf[User]("Processing user: %+v")
IO.Logf[int]("Count: %d")

Logger — with custom *log.Logger

IO.Logger[A any](loggers ...*log.Logger) func(prefix string) func(A) IO[A]

Uses logger.Printf(prefix+": %v", value). Pass your own *log.Logger instance.

customLog := log.New(os.Stderr, "APP ", log.LstdFlags)
logUser := IO.Logger[User](customLog)("user")
// logs: "APP user: {ID:42 Name:Alice}"

LogGo — Go template syntax

IO.LogGo[A any](tmpl string) func(A) IO[A]

Uses Go's text/template. The template receives the value as ..

type User struct{ Name string; Age int }
IO.LogGo[User]("User {{.Name}} is {{.Age}} years old")

Printf / PrintGo — stdout instead of log

Same signatures as Logf / LogGo but use fmt.Printf/fmt.Println (no log prefix, no timestamp).

IO.Printf[Result]("Result: %v\n")
IO.PrintGo[User]("Name: {{.Name}}")

Structured Logging in the context Package

The context/readerioresult, context/readerresult, and context/readerio packages provide structured slog-based logging functions that are context-aware: they retrieve the logger from the context (via logging.GetLoggerFromContext) rather than using a fixed logger instance.

TapSLog — inline structured logging in a ReaderIOResult pipeline

TapSLog is an Operator (func(ReaderIOResult[A]) ReaderIOResult[A]). It sits directly in a F.Pipe call on a ReaderIOResult, logs the current value or error using slog, and passes the result through unchanged.

import (
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    F   "github.com/IBM/fp-go/v2/function"
)

pipeline := F.Pipe4(
    fetchOrder(orderID),
    RIO.TapSLog[Order]("Order fetched"),        // logs value=<Order> or error=<err>
    RIO.Chain(validateOrder),
    RIO.TapSLog[Order]("Order validated"),
    RIO.Chain(processPayment),
)

order, err := result.Unwrap(pipeline(ctx)())
  • Logs both success values (value=<A>) and errors (error=<err>) using slog structured attributes.
  • Respects the logger level — if the logger is configured to discard Info-level logs, nothing is written.
  • Available in both context/readerioresult and context/readerresult.

SLog, SLogRight, SLogLeft — the underlying Kleisli arrows

In context/readerioresult, SLog[A](msg) is Kleisli[Result[A], Void] — it takes the raw Result[A] and returns Void. It is the building block TapSLog is made from (TapSLog = readerio.Tap ∘ SLog), and it cannot be dropped into RIO.Chain or RIO.ChainFirst on a ReaderIOResult[A]: those expect a Kleisli over A, not over Result[A].

// ❌ WRONG — does not compile; SLog's input is Result[Data], its output is Void
RIO.Chain(RIO.SLog[Data]("Data fetched"))

// ✅ Log value and error, pass the value through — this is what you almost always want
RIO.TapSLog[Data]("Data fetched")

// ✅ Log only the success branch via ChainFirst — SLogRight is Kleisli[A, Void]
RIO.ChainFirst(RIO.SLogRight[Data]("Data fetched"))
Function Shape Use
TapSLog[A](msg) Operator[A, A] drop directly into F.Pipe — logs value or error, value flows through
TapSLogInfo[A] / TapSLogDebug[A] Operator[A, A] same, at an explicit level
SLogRight[A](msg) Kleisli[A, Void] usable with RIO.ChainFirst — success branch only
SLogLeft(msg) Kleisli[error, Void] usable with RIO.ChainFirstLeft[A] — error branch only (annotate A)
SLog[A](msg) / SLogInfo / SLogDebug Kleisli[Result[A], Void] low-level; prefer TapSLog

In context/readerresult the shape differs: there SLog[A](msg) is Kleisli[Result[A], A] (it passes the value on rather than returning Void). TapSLog[A](msg) Operator[A, A] is identical in both packages — prefer it and the difference never bites.

SLogWithCallback — custom log level and logger source

import (
    RIO  "github.com/IBM/fp-go/v2/context/readerioresult"
    "log/slog"
)

// Log at DEBUG level with a custom logger extracted from context
debugLog := RIO.SLogWithCallback[User](
    slog.LevelDebug,
    logging.GetLoggerFromContext, // or any func(context.Context) *slog.Logger
    "Fetched user",
)

pipeline := F.Pipe2(
    fetchUser(123),
    RIO.Chain(debugLog),
    RIO.Map(func(u User) string { return u.Name }),
)

LogEntryExit — automatic entry/exit timing with correlation IDs

LogEntryExit wraps a ReaderIOResult computation with structured entry and exit log messages. It assigns a unique correlation ID (ID=<n>) to each invocation so concurrent or nested operations can be correlated in logs.

import (
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    F   "github.com/IBM/fp-go/v2/function"
)

pipeline := F.Pipe3(
    fetchUser(123),
    RIO.LogEntryExit[User]("fetchUser"),   // wraps the operation
    RIO.Chain(func(user User) RIO.ReaderIOResult[[]Order] {
        return F.Pipe1(
            fetchOrders(user.ID),
            RIO.LogEntryExit[[]Order]("fetchOrders"),
        )
    }),
)

orders, err := result.Unwrap(pipeline(ctx)())
// Logs:
// level=INFO msg="[entering]" name=fetchUser ID=1
// level=INFO msg="[exiting ]" name=fetchUser ID=1 duration=42ms
// level=INFO msg="[entering]" name=fetchOrders ID=2
// level=INFO msg="[exiting ]" name=fetchOrders ID=2 duration=18ms

On error, the exit log changes to [throwing] and includes the error:

level=INFO msg="[throwing]" name=fetchUser ID=3 duration=5ms error="user not found"

Key properties:

  • Correlation ID (ID=) is unique per operation, monotonically increasing, and stored in the context so nested operations can access the parent's ID.
  • Duration (duration=) is measured from entry to exit.
  • Logger is taken from the context — embed a request-scoped logger with logging.WithLogger before executing the pipeline and LogEntryExit picks it up automatically.
  • Level-aware — if the logger does not have the log level enabled, the entire entry/exit instrumentation is skipped (zero overhead).
  • The original ReaderIOResult[A] value flows through unchanged.
// Use a context logger so all log messages carry request metadata
cancelFn, ctxWithLogger := pair.Unpack(
    logging.WithLogger(
        slog.Default().With("requestID", r.Header.Get("X-Request-ID")),
    )(r.Context()),
)
defer cancelFn()

value, err := result.Unwrap(pipeline(ctxWithLogger)())

LogEntryExitWithCallback — custom log level

import (
    RIO  "github.com/IBM/fp-go/v2/context/readerioresult"
    "log/slog"
)

// Log at DEBUG level instead of INFO
debugPipeline := F.Pipe1(
    expensiveComputation(),
    RIO.LogEntryExitWithCallback[Result](
        slog.LevelDebug,
        logging.GetLoggerFromContext,
        "expensiveComputation",
    ),
)

SLog / SLogWithCallback in context/readerresult

The same SLog and TapSLog functions are also available in context/readerresult for use with the synchronous ReaderResult[A] = func(context.Context) (A, error) monad:

import RR "github.com/IBM/fp-go/v2/context/readerresult"

pipeline := F.Pipe3(
    queryDB(id),
    RR.TapSLog[Row]("Row fetched"),
    RR.Chain(parseRow),
    RR.TapSLog[Record]("Record parsed"),
)

Global Logger (logging package)

The logging package manages a global *slog.Logger (structured logging, Go 1.21+).

import "github.com/IBM/fp-go/v2/logging"

// Get the current global logger (defaults to slog.Default())
logger := logging.GetLogger()
logger.Info("application started", "version", "1.0")

// Replace the global logger; returns the old one for deferred restore
old := logging.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
defer logging.SetLogger(old)

Context-Embedded Logger

Embed a *slog.Logger in a context.Context to carry request-scoped loggers across the call stack. All context-package logging functions (TapSLog, SLog, LogEntryExit) pick up this logger automatically.

import (
    "github.com/IBM/fp-go/v2/logging"
    "github.com/IBM/fp-go/v2/pair"
    "log/slog"
)

// Create a request-scoped logger
reqLogger := slog.Default().With("requestID", "abc-123")

// Embed it into a context using the Kleisli arrow WithLogger
cancelFn, ctxWithLogger := pair.Unpack(logging.WithLogger(reqLogger)(ctx))
defer cancelFn()

// All downstream logging (TapSLog, LogEntryExit, etc.) uses reqLogger
value, err := result.Unwrap(pipeline(ctxWithLogger)())

WithLogger returns a ContextCancel = Pair[context.CancelFunc, context.Context]. The cancel function is a no-op — the context is only enriched, not made cancellable.

GetLoggerFromContext falls back to the global logger if no logger is found in the context.

LoggingCallbacks — Dual-Logger Pattern

import "github.com/IBM/fp-go/v2/logging"

// Returns (infoCallback, errorCallback) — both are func(string, ...any)
infoLog, errLog := logging.LoggingCallbacks()                    // use log.Default() for both
infoLog, errLog := logging.LoggingCallbacks(myLogger)            // same logger for both
infoLog, errLog := logging.LoggingCallbacks(infoLog, errorLog)   // separate loggers

Used internally by io.Logger and by packages that need separate info/error sinks.

Choosing the Right Logging Function

Situation Use
Quick printf logging mid-pipeline IO.Logf[A]("fmt") with ChainFirstIOK
Go template formatting mid-pipeline IO.LogGo[A]("tmpl") with ChainFirstIOK
Print to stdout (no log prefix) IO.Printf[A]("fmt") with ChainFirstIOK
Structured slog — log value or error inline RIO.TapSLog[A]("msg") (Operator, used in Pipe)
Structured slog — success branch only RIO.ChainFirst(RIO.SLogRight[A]("msg"))
Structured slog — error branch only RIO.ChainFirstLeft(RIO.SLogLeft("msg"))
Structured slog — custom log level RIO.SLogWithCallback[A](level, cb, "msg")
Entry/exit timing + correlation IDs RIO.LogEntryExit[A]("name")
Entry/exit at custom log level RIO.LogEntryExitWithCallback[A](level, cb, "name")
Structured logging globally logging.GetLogger() / logging.SetLogger()
Request-scoped logger in context logging.WithLogger(logger) + logging.GetLoggerFromContext(ctx)
Custom *log.Logger in pipeline IO.Logger[A](logger)("prefix") with ChainFirstIOK

Complete Example

package main

import (
    "context"
    "log/slog"
    "os"

    F   "github.com/IBM/fp-go/v2/function"
    IO  "github.com/IBM/fp-go/v2/io"
    L   "github.com/IBM/fp-go/v2/logging"
    P   "github.com/IBM/fp-go/v2/pair"
    RIO "github.com/IBM/fp-go/v2/context/readerioresult"
    "github.com/IBM/fp-go/v2/result"
)

func main() {
    // Configure JSON structured logging globally
    L.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

    // Embed a request-scoped logger into the context
    _, ctx := P.Unpack(L.WithLogger(
        L.GetLogger().With("requestID", "req-001"),
    )(context.Background()))

    pipeline := F.Pipe5(
        fetchData(42),
        RIO.LogEntryExit[Data]("fetchData"),                  // entry/exit with timing + ID
        RIO.TapSLog[Data]("raw data"),                        // inline structured value log
        RIO.ChainResultK(result.Eitherize1(transformData)),   // func(Data) (Out, error)
        RIO.LogEntryExit[Out]("transformData"),
        RIO.ChainFirstIOK(IO.LogGo[Out]("result: {{.Value}}")), // template log
    )

    // pipeline(ctx)() is a single Result[Out]; unwrap it for idiomatic Go
    value, err := result.Unwrap(pipeline(ctx)())
    if err != nil {
        L.GetLogger().Error("pipeline failed", "error", err)
    }
    _ = value
}