Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

clog

A highly customizable structured logger for command-line tools with a zerolog-inspired fluent API, terminal-aware colours, hyperlinks, and animations.

clog demo

Features

Installation

go get github.com/gechr/clog

Quick Start

package main

import (
  "fmt"

  "github.com/gechr/clog"
)

func main() {
  clog.Info().Str("port", "8080").Msg("Server started")
  clog.Warn().Str("path", "/old").Msg("Deprecated endpoint")
  err := fmt.Errorf("connection refused")
  clog.Error().Err(err).Msg("Connection failed")
}

Output:

INF ℹ️ Server started port=8080
WRN ⚠️ Deprecated endpoint path=/old
ERR ❌ Connection failed error=connection refused

Levels

LevelValueLabelPrefixDescription
Trace-10TRC🔍Finest-grained output, hidden by default
Debug-5DBG🐞Verbose output, hidden by default
Info0INFℹ️General operational messages (default minimum level)
Dry2DRY🚧Dry-run indicators
Warn5WRN⚠️Warnings that don’t prevent operation
Error10ERRErrors that need attention
Fatal15FTL💥Fatal errors - calls os.Exit(1) after logging

Built-in levels use gaps of 5 between them (except around Dry, which sits at 2), leaving room for custom levels at any position (see Custom Levels).

Setting the Level

// Programmatically
clog.SetLevel(clog.DebugLevel)

// From environment variable (CLOG_LOG_LEVEL is checked automatically on init)
// export CLOG_LOG_LEVEL=debug

Recognised CLOG_LOG_LEVEL values: trace, debug, info, dry, warn, warning, error, fatal, critical.

Setting trace or debug also enables timestamps.

Parsing Levels

ParseLevel converts a string to a Level value (case-insensitive):

level, err := clog.ParseLevel("debug")

Level implements encoding.TextMarshaler and encoding.TextUnmarshaler, so it works directly with flag.TextVar and most flag libraries.

Custom Levels

Define custom levels at any numeric value between the built-in levels. Use RegisterLevel to configure the label, prefix, style, and canonical name.

const SuccessLevel clog.Level = clog.InfoLevel + 1

func init() {
    clog.RegisterLevel(SuccessLevel, clog.LevelConfig{
        Name:   "success",
        Label:  "SCS",
        Prefix: "✅",
        Style:  new(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("2"))),
    })
}

Log with clog.Log(level):

clog.Log(SuccessLevel).Msg("Build completed")
// SCS ✅ Build completed

Custom levels respect level filtering based on their numeric value. ParseLevel, MarshalText, and UnmarshalText all work with registered custom levels.

Use clog.Levels() to iterate all registered levels (built-in and custom) in ascending severity order.

Structured Fields

Events and contexts support typed field methods. All methods are safe to call on a nil receiver (disabled events are no-ops).

Event Fields

MethodSignatureDescription
AnyAny(key string, val any)Arbitrary value
AnysAnys(key string, vals []any)Arbitrary value slice
Base64Base64(key string, val []byte)Byte slice as base64 string
BoolBool(key string, val bool)Boolean field
BoolsBools(key string, vals []bool)Boolean slice field
BytesBytes(key string, val []byte)Byte slice - auto-detected as JSON with highlighting, otherwise string
ColumnColumn(key, path string, line, column int)Clickable file:line:column hyperlink
DictDict(key string, dict *Event)Nested fields with dot-notation keys
DurationDuration(key string, val time.Duration)Duration field
DurationsDurations(key string, vals []time.Duration)Duration slice field
ErrErr(err error)Attach error; Send uses it as message, Msg/Msgf add "error" field
ErrsErrs(key string, vals []error)Error slice as string slice (nil errors render as <nil>)
Float64Float64(key string, val float64)Float field
Floats64Floats64(key string, vals []float64)Float slice field
FuncFunc(fn func(*Event))Lazy field builder; callback skipped on nil (disabled) events
HexHex(key string, val []byte)Byte slice as hex string
IntInt(key string, val int)Integer field
Int64Int64(key string, val int64)64-bit integer field
IntsInts(key string, vals []int)Integer slice field
Ints64Ints64(key string, vals []int64)64-bit integer slice field
JSONJSON(key string, val any)Marshals val to JSON with syntax highlighting
LineLine(key, path string, line int)Clickable file:line hyperlink
LinkLink(key, url, text string)Clickable URL hyperlink
PathPath(key, path string)Clickable file/directory hyperlink
PercentPercent(key string, val float64, opts ...PercentOption)Percentage with gradient colour; accepts [PercentOption] values
QuantitiesQuantities(key string, vals []string)Quantity slice field
QuantityQuantity(key, val string)Quantity field (e.g. "10GB")
RawJSONRawJSON(key string, val []byte)Pre-serialized JSON bytes, emitted verbatim with syntax highlighting
StrStr(key, val string)String field
StringerStringer(key string, val fmt.Stringer)Calls String() (nil-safe)
StringersStringers(key string, vals []fmt.Stringer)Slice of fmt.Stringer values
StrsStrs(key string, vals []string)String slice field
TimeTime(key string, val time.Time)Time field
TimesTimes(key string, vals []time.Time)Time slice field
UintUint(key string, val uint)Unsigned integer field
Uint64Uint64(key string, val uint64)64-bit unsigned integer field
UintsUints(key string, vals []uint)Unsigned integer slice field
Uints64Uints64(key string, vals []uint64)64-bit unsigned integer slice field
URLURL(key, url string)Clickable URL hyperlink (URL as text)
WhenWhen(condition bool, fn func(*Event))Conditional field builder; fn called only when condition is true

Nested Fields (Dict)

Group related fields under a common key prefix using dot notation:

clog.Info().Dict("request", clog.Dict().
  Str("method", "GET").
  Int("status", 200),
).Msg("Handled")
// INF ℹ️ Handled request.method=GET request.status=200

Works with sub-loggers too:

logger := clog.With().Dict("db", clog.Dict().
  Str("host", "localhost").
  Int("port", 5432),
).Logger()

Finalising Events

clog.Info().Str("k", "v").Msg("message")  // Log with message
clog.Info().Str("k", "v").Msgf("n=%d", 5) // Log with formatted message
clog.Info().Str("k", "v").Send()          // Log with empty message
clog.Error().Err(err).Send()              // Log with error as message (no error= field)
clog.Error().Err(err).Msg("failed")       // Log with message + error= field

Sub-loggers & Context

Sub-loggers

Create sub-loggers with preset fields using the With() context builder:

logger := clog.With().Str("component", "auth").Logger()
logger.Info().Str("user", "john").Msg("Authenticated")
// INF ℹ️ Authenticated component=auth user=john

Context fields support the same typed methods as events.

Context Propagation

Store a logger in a context.Context and retrieve it deeper in the call stack:

logger := clog.With().Str("request_id", "abc-123").Logger()
ctx := logger.WithContext(ctx)

// later, in any function that receives ctx:
clog.Ctx(ctx).Info().Msg("Handling request")
// INF ℹ️ Handling request request_id=abc-123

Ctx returns clog.Default when the context is nil or contains no logger, so it is always safe to call.

A package-level WithContext convenience stores clog.Default:

ctx := clog.WithContext(ctx) // stores clog.Default

Omit Empty / Zero

OmitEmpty

OmitEmpty omits fields that are semantically “nothing”: nil, empty strings "", and nil or empty slices and maps.

clog.SetOmitEmpty(true)
clog.Info().
  Str("name", "alice").
  Str("nickname", "").   // omitted
  Any("role", nil).      // omitted
  Int("age", 0).         // kept (zero but not empty)
  Bool("admin", false).  // kept (zero but not empty)
  Msg("User")
// INF ℹ️ User name=alice age=0 admin=false

OmitZero

OmitZero is a superset of OmitEmpty - it additionally omits 0, false, 0.0, zero durations, and any other typed zero value.

clog.SetOmitZero(true)
clog.Info().
  Str("name", "alice").
  Str("nickname", "").   // omitted
  Any("role", nil).      // omitted
  Int("age", 0).         // omitted
  Bool("admin", false).  // omitted
  Msg("User")
// INF ℹ️ User name=alice

Both settings are inherited by sub-loggers created with With(). When both are enabled, OmitZero takes precedence.

Quoting

By default, field values containing spaces or special characters are wrapped in Go-style double quotes ("hello world"). This behaviour can be customised with SetQuoteMode.

Quote Modes

ModeDescription
QuoteAutoQuote only when needed - spaces, unprintable chars, embedded quotes (default)
QuoteAlwaysAlways quote string, error, and default-kind values
QuoteNeverNever quote
// Default: only quote when needed
clog.Info().Str("reason", "timeout").Str("msg", "hello world").Msg("test")
// INF ℹ️ test reason=timeout msg="hello world"

// Always quote string values
clog.SetQuoteMode(clog.QuoteAlways)
clog.Info().Str("reason", "timeout").Msg("test")
// INF ℹ️ test reason="timeout"

// Never quote
clog.SetQuoteMode(clog.QuoteNever)
clog.Info().Str("msg", "hello world").Msg("test")
// INF ℹ️ test msg=hello world

Custom Quote Character

Use a different character for both sides:

clog.SetQuoteChar('\'')
clog.Info().Str("msg", "hello world").Msg("test")
// INF ℹ️ test msg='hello world'

Asymmetric Quote Characters

Use different opening and closing characters:

clog.SetQuoteChars('«', '»')
clog.Info().Str("msg", "hello world").Msg("test")
// INF ℹ️ test msg=«hello world»

clog.SetQuoteChars('[', ']')
clog.Info().Str("msg", "hello world").Msg("test")
// INF ℹ️ test msg=[hello world]

Quoting applies to individual field values and to elements within string and []any slices. All quoting settings are inherited by sub-loggers. Pass 0 to reset to the default (strconv.Quote).

Custom Prefix

Override the default emoji prefix per-event, per-logger, or globally:

// Per-event
clog.Info().Prefix("📦").Str("pkg", "clog").Msg("Installed")

// Per-logger (via sub-logger)
logger := clog.With().Prefix("🛡️").Str("component", "auth").Logger()
logger.Info().Msg("Ready")

// Global (changes defaults for all levels)
clog.SetPrefixes(clog.LevelMap{
  clog.InfoLevel:  ">>",
  clog.WarnLevel:  "!!",
  clog.ErrorLevel: "XX",
})

Prefix resolution order: event override > logger preset > default emoji for level.

Missing levels in SetPrefixes fall back to the defaults. Use DefaultPrefixes() to get a copy of the default prefix map.

Styling Prefixes

Prefixes can be any string - not just emojis. Use Styles.Prefixes to apply a lipgloss style per level:

styles := clog.DefaultStyles()

// Render the warn prefix in bold yellow
styles.Prefixes[clog.WarnLevel] = new(
  lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3")), // yellow
)

clog.SetStyles(styles)

// Both "warning" and "!!" are printed in bold yellow
clog.Warn().Prefix("warning").Msg("Low disk space")
clog.Warn().Prefix("!!").Msg("Low disk space")

Styles.Prefixes is a LevelStyleMap. Entries for levels not in the map render unstyled (the default). Use nil for a specific level to explicitly disable styling for that level.

Custom Labels

Override the default level labels with SetLevelLabels:

clog.SetLevelLabels(clog.LevelMap{
  clog.InfoLevel:  "INFO",
  clog.WarnLevel:  "WARNING",
  clog.ErrorLevel: "ERROR",
})

Missing levels fall back to the defaults. Use DefaultLabels() to get a copy of the default label map.

Level Alignment

When custom labels have different widths, control alignment with SetLevelAlign:

clog.SetLevelAlign(clog.AlignRight)   // default: "   INFO", "WARNING", "  ERROR"
clog.SetLevelAlign(clog.AlignLeft)    //          "INFO   ", "WARNING", "ERROR  "
clog.SetLevelAlign(clog.AlignCenter)  //          " INFO  ", "WARNING", " ERROR "
clog.SetLevelAlign(clog.AlignNone)    //          "INFO",    "WARNING", "ERROR"

Custom Levels

Define custom log levels at any numeric value between the built-in levels. Built-in levels use gaps of 5, so there is plenty of room.

Registering a Custom Level

const SuccessLevel clog.Level = clog.InfoLevel + 1

func init() {
    clog.RegisterLevel(SuccessLevel, clog.LevelConfig{
        Name:   "success", // required: canonical name for ParseLevel/MarshalText
        Label:  "SCS", // short display label (default: uppercase Name, max 3 chars)
        Prefix: "✅", // emoji prefix (default: "")
        Style:  new(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("2"))),
    })
}

Logging with a Custom Level

Use Log(level) instead of the named methods:

clog.Log(SuccessLevel).Str("pkg", "api").Msg("Build completed")
// SCS ✅ Build completed pkg=api

Level Filtering

Custom levels respect the same filtering rules as built-in levels. A custom level with value 1 (between InfoLevel at 0 and DryLevel at 2) is visible when the minimum level is InfoLevel but hidden when the minimum level is DryLevel or higher.

ParseLevel and Marshalling

Registered custom levels work with ParseLevel, MarshalText, and UnmarshalText:

level, err := clog.ParseLevel("success") // returns SuccessLevel

Iterating All Levels

Since built-in levels use gaps, level++ iteration will not work. Use Levels() instead:

for _, level := range clog.Levels() {
    fmt.Println(level) // prints all levels in ascending severity order
}

Part Order

Control which parts appear in log output and in what order. The default order is: timestamp, level, prefix, message, fields.

// Reorder: show message before level
clog.SetParts(clog.PartMessage, clog.PartLevel, clog.PartPrefix, clog.PartFields)

// Hide parts by omitting them
clog.SetParts(clog.PartLevel, clog.PartMessage, clog.PartFields) // no prefix or timestamp

// Fields before message
clog.SetParts(clog.PartLevel, clog.PartFields, clog.PartMessage)

Available parts: PartTimestamp, PartLevel, PartPrefix, PartMessage, PartFields.

Use DefaultParts() to get the default ordering. Parts omitted from the list are hidden.

Per-Event Override

Override the part order for a single log event without mutating the logger:

// This event shows only the prefix and message - no level label or fields.
clog.Info().Parts(clog.PartPrefix, clog.PartMessage).Msg("hello")

// Other events still use the logger's default parts.
clog.Info().Msg("world")

This is useful when a particular message needs different formatting (e.g. a banner or status line) without creating a separate logger or calling SetParts.

The same .Parts() method is available on animations and their results:

// Spinner - both animation and completion use overridden parts.
clog.Spinner("loading").
  Parts(clog.PartPrefix, clog.PartMessage).
  Wait(ctx, fn).
  Msg("done")

// Override only on the completion message.
clog.Spinner("loading").
  Wait(ctx, fn).
  Parts(clog.PartMessage).
  Msg("done")

See Spinner and Group for more details.

Elapsed

Measure and log the elapsed duration of an operation without animations:

e := clog.Info().Elapsed("elapsed")
runMigrations()
e.Msg("database migration")
// INF ℹ️ database migration elapsed=2s

Use Elapsed when you want elapsed-time logging but don’t need a spinner, progress bar, or any visual animation. The elapsed field uses the same formatting, styling, and field type as animation Elapsed fields.

Finalising

Elapsed events can be finalised with Send (no message), Msg, or Msgf:

e := clog.Info().Elapsed("elapsed")
runMigrations()
e.Send()
// INF ℹ️ elapsed=2s

e = clog.Info().Elapsed("elapsed")
runMigrations()
e.Msg("migrations complete")
// INF ℹ️ migrations complete elapsed=2s

Field Positioning

Elapsed can appear anywhere in the field chain to control where the elapsed field is shown:

// Elapsed before other fields
e := clog.Info().Elapsed("elapsed").Str("env", "prod")
deploy()
e.Msg("deploy")
// INF ℹ️ deploy elapsed=12s env=prod

// Elapsed after other fields
e = clog.Info().Str("env", "prod").Elapsed("elapsed")
deploy()
e.Msg("deploy")
// INF ℹ️ deploy env=prod elapsed=12s

Custom Key

The key parameter controls the field name:

e := clog.Info().Elapsed("duration")
compile()
e.Msg("compile")
// INF ℹ️ compile duration=3s

Log Levels

Since Elapsed is a method on events, you control the level directly:

clog.Warn().Elapsed("elapsed").Msg("slow query")
// WRN ⚠️ slow query elapsed=5s

clog.Error().Elapsed("elapsed").Err(err).Msg("compile")
// ERR ❌ compile elapsed=3s error="syntax error"

Sub-loggers

Elapsed works on any logger instance:

logger := clog.With().Str("component", "db").Logger()
e := logger.Info().Elapsed("elapsed")
runQuery()
e.Msg("query")
// INF ℹ️ query component=db elapsed=1s

Elapsed Configuration

The elapsed field respects the same configuration as animation elapsed fields:

MethodDefaultDescription
SetElapsedPrecision0Decimal places (0 = 3s, 1 = 3.2s)
SetElapsedRoundtime.SecondRounding granularity (0 disables rounding)
SetElapsedMinimumtime.SecondHide elapsed field below this threshold
SetElapsedFormatFuncnil (built-in)Custom format function for elapsed durations

Indentation

Create visually nested output where the message text shifts right while level, timestamp, and prefix columns stay fixed.

SetIndent

Set the indent depth directly on any logger:

logger.SetIndent(1)
logger.Info().Msg("indented")
// INF ℹ️   indented

logger.SetIndent(0) // back to normal

Sub-logger Indent

Use Indent() on a context builder to add one indent level (2 spaces by default). It’s chainable:

clog.Info().Msg("Building project")

build := clog.With().Indent().Logger()
build.Info().Msg("Compiling main.go")
build.Info().Msg("Compiling util.go")

link := build.With().Indent().Logger()
link.Info().Msg("Linking binary")

clog.Info().Msg("Build complete")
INF ℹ️ Building project
INF ℹ️   Compiling main.go
INF ℹ️   Compiling util.go
INF ℹ️     Linking binary
INF ℹ️ Build complete

Dedent

Use Dedent() to remove one indent level from a context builder. It is the mirror of Indent() and clamps at zero - calling it on an unindented logger is a no-op:

build := clog.With().Depth(3).Logger()

// Step back one level for a less-nested sub-logger.
back := build.With().Dedent().Logger()
back.Info().Msg("one step back")
// INF ℹ️     one step back  (depth 2, not 3)

// Indent and Dedent cancel out.
same := clog.With().Indent().Dedent().Logger()
same.Info().Msg("no change")
// INF ℹ️ no change

Dedent() is also available on animation builders:

clog.Spinner("loading").Indent().Indent().Dedent(). // net depth 1
    Wait(ctx, work).Msg("done")

Depth

Use Depth(n) to add multiple indent levels at once:

sub := clog.With().Depth(3).Logger()
sub.Info().Msg("Deeply nested")
// INF ℹ️       Deeply nested

Indent Width

The default indent width is 2 spaces per level. Change it with SetIndentWidth:

clog.SetIndentWidth(4)

Indent Prefixes

Add decorations before the message at each indent level with SetIndentPrefixes. Prefixes cycle through the slice and a separator (default " ") is appended automatically:

clog.SetIndentPrefixes([]string{"│"})

sub := clog.With().Indent().Logger()
sub.Info().Msg("hello")
// INF ℹ️   │ hello

With multiple prefixes they cycle per depth:

clog.SetIndentPrefixes([]string{"├─", "└─"})

d1 := clog.With().Indent().Logger()
d1.Info().Msg("first")
// INF ℹ️   ├─ first

d2 := clog.With().Depth(2).Logger()
d2.Info().Msg("second")
// INF ℹ️     └─ second

d3 := clog.With().Depth(3).Logger()
d3.Info().Msg("third")  // wraps back to first prefix
// INF ℹ️       ├─ third

Prefix Separator

Change the separator between the prefix and message with SetIndentPrefixSeparator:

clog.SetIndentPrefixSeparator("")   // no space
clog.SetIndentPrefixSeparator("  ") // double space

Handlers

Custom handlers receive the indent level via Entry.Indent:

clog.SetHandler(clog.HandlerFunc(func(e clog.Entry) {
    fmt.Printf("indent=%d msg=%s\n", e.Indent, e.Message)
}))

Tree Indentation

Draw tree-like structures with box-drawing connectors (├──, └──, ). Each call to Tree() adds one nesting level, and ancestor continuation lines are handled automatically.

Basic Usage

Use Tree() on a context builder with TreeFirst, TreeMiddle, or TreeLast:

clog.Info().Msg("Project")

src := clog.With().Tree(clog.TreeMiddle).Logger()
src.Info().Msg("src/")

main := src.With().Tree(clog.TreeMiddle).Logger()
main.Info().Msg("main.go")

util := src.With().Tree(clog.TreeLast).Logger()
util.Info().Msg("util.go")

mod := clog.With().Tree(clog.TreeLast).Logger()
mod.Info().Msg("go.mod")
INF ℹ️ Project
INF ℹ️ ├── src/
INF ℹ️ │   ├── main.go
INF ℹ️ │   └── util.go
INF ℹ️ └── go.mod

Positions

PositionConnectorMeaning
TreeFirst├──First sibling
TreeMiddle├──Middle sibling
TreeLast└──Last sibling (no more)

TreeFirst and TreeMiddle render identically by default but can be distinguished with custom characters (see below).

Combining with Indent

Tree and indent are orthogonal. When both are present, indent spaces render first, then tree connectors:

sub := clog.With().Indent().Tree(clog.TreeMiddle).Logger()
sub.Info().Msg("hello")
// INF ℹ️   ├── hello

Custom Characters

Override the default box-drawing characters with SetTreeChars:

clog.SetTreeChars(clog.TreeChars{
    First:    "┌─ ",
    Middle:   "├─ ",
    Last:     "└─ ",
    Continue: "│  ",
    Blank:    "   ",
})
FieldDefaultPurpose
First├──Connector for TreeFirst
Middle├──Connector for TreeMiddle
Last└──Connector for TreeLast
ContinueAncestor line when parent is First/Middle
BlankAncestor line when parent is Last

Animations

Tree positions work on animation builders too:

clog.Spinner("loading").Tree(clog.TreeMiddle).Wait(ctx, work).Msg("done")

Handlers

Custom handlers receive the tree positions via Entry.Tree:

clog.SetHandler(clog.HandlerFunc(func(e clog.Entry) {
    fmt.Printf("tree=%v msg=%s\n", e.Tree, e.Message)
}))

Dividers

Print horizontal rules to visually separate sections of CLI output:

clog.Divider().Send()
// ────────────────────────────────────────────────────────────────────────────────

Title

Add a title to label the section:

clog.Divider().Msg("Build Phase")
// ─── Build Phase ────────────────────────────────────────────────────────────────

Custom Character

Change the line character:

clog.Divider().Char('═').Send()
// ════════════════════════════════════════════════════════════════════════════════

clog.Divider().Char('═').Msg("Deployment")
// ═══ Deployment ═════════════════════════════════════════════════════════════════

Title Alignment

Control where the title sits within the line:

// Left-aligned (default)
clog.Divider().Msg("Left")
// ─── Left ───────────────────────────────────────────────────────────────────────

// Centered
clog.Divider().Align(clog.AlignCenter).Msg("Center")
// ───────────────────────────────── Center ───────────────────────────────────────

// Right-aligned
clog.Divider().Align(clog.AlignRight).Msg("Right")
// ────────────────────────────────────────────────────────────────────── Right ───

Width

The divider fills the full terminal width. When the terminal width cannot be detected (non-TTY output, piped commands), it defaults to 80 columns.

Override the width manually with Width:

clog.Divider().Width(40).Send()
// ────────────────────────────────────────

clog.Divider().Width(40).Msg("Short")
// ─── Short ──────────────────────────────

Styling

Divider styles are configured through the Styles struct:

styles := clog.DefaultStyles()
styles.DividerLine = new(lipgloss.NewStyle().Foreground(lipgloss.Color("4")))   // blue line
styles.DividerTitle = new(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("3"))) // bold yellow title
clog.SetStyles(styles)
Style FieldDefaultDescription
DividerLineFaintStyle for the line characters
DividerTitleBoldStyle for the title text

Spinner

Display animated spinners during long-running operations:

err := clog.Spinner("Downloading").
  Str("url", fileURL).
  Wait(ctx, func(ctx context.Context) error {
    return download(ctx, fileURL)
  }).
  Msg("Downloaded")

The spinner animates with moon phase emojis (🌔🌓🌒🌑🌘🌗🌖🌕) while the action runs, then logs the result. This is the DefaultSpinnerStyle, which is used when no custom Style is set.

Spinner demo

Dynamic Status Updates

Use Progress to update the spinner message and fields during execution:

err := clog.Spinner("Processing").
  Progress(ctx, func(ctx context.Context, update *clog.ProgressUpdate) error {
    for i, item := range items {
      update.Msg("Processing").Str("progress", fmt.Sprintf("%d/%d", i+1, len(items))).Send()
      if err := process(ctx, item); err != nil {
        return err
      }
    }
    return nil
  }).
  Msg("Processed all items")

WaitResult Finalisers

MethodSuccess behaviourFailure behaviour
.Msg(s)Logs at INF with messageLogs at ERR with error string
.Err()Logs at INF with spinner messageLogs at ERR with error string as msg
.Send()Logs at configured levelLogs at configured level
.Silent()Returns error, no loggingReturns error, no logging

.Err() is equivalent to calling .Send() with default settings (no OnSuccess/OnError overrides).

All finalisers return the error from the action. You can chain any field method (.Str(), .Int(), .Bool(), .Duration(), etc.) and .Prefix() on a WaitResult before finalising.

Custom Success/Error Behaviour

Use OnSuccessLevel, OnSuccessMessage, OnErrorLevel, and OnErrorMessage to customise how the result is logged, then call .Send():

// Fatal on error instead of the default error level
err := clog.Spinner("Connecting to database").
  Str("host", "db.internal").
  Wait(ctx, connectToDB).
  OnErrorLevel(clog.FatalLevel).
  Send()

When OnErrorMessage is set, the custom message becomes the log message and the original error is included as an error= field. Without it, the error string is used directly as the message with no extra field.

Custom Spinner Style

clog.Spinner("Loading").
  Style(clog.SpinnerDot).
  Wait(ctx, action).
  Msg("Done")

See progress_spinner_presets.go for the full list of available spinner types.

Spinner styles 1 Spinner styles 2 Spinner styles 3 Spinner styles 4 Spinner styles 5 Spinner styles 6 Spinner styles 7 Spinner styles 8 Spinner styles 9

The AnimationBuilder supports the same clickable hyperlink field methods as events:

clog.Spinner("Building").
  Path("dir", "src/").
  Line("config", "config.yaml", 42).
  Column("loc", "main.go", 10, 5).
  URL("docs", "https://example.com").
  Link("help", "https://example.com", "docs").
  Wait(ctx, action).
  Msg("Built")

Elapsed Timer

Add a live elapsed-time field to any animation with .Elapsed(key):

err := clog.Spinner("Processing batch").
  Str("batch", "1/3").
  Elapsed("elapsed").
  Int("workers", 4).
  Wait(ctx, processBatch).
  Msg("Batch processed")
// INF ✅ Batch processed batch=1/3 elapsed=2s workers=4

The elapsed field respects its position relative to other field methods - it appears between batch and workers in the output above because .Elapsed("elapsed") was called between .Str() and .Int().

The display format uses SetElapsedPrecision (default 0 decimal places), rounds to SetElapsedRound (default 1s), hides values below SetElapsedMinimum (default 1s), and can be fully overridden with SetElapsedFormatFunc. Durations >= 1m use composite format (e.g. “1m30s”, “2h15m”).

Per-Event Parts Override

Override the part order for a spinner and its completion message without mutating the logger:

err := clog.Spinner("Indexing files").
  Parts(clog.PartPrefix, clog.PartMessage).
  Wait(ctx, indexFiles).
  Msg("Indexed")
// ✅ Indexed   (no level label or fields)

When set on the AnimationBuilder, the override applies to both the animation rendering and the default completion message. You can further override on the WaitResult if the completion needs different parts:

clog.Spinner("Syncing").
  Parts(clog.PartMessage).          // animation: message only
  Wait(ctx, sync).
  Parts(clog.PartLevel, clog.PartMessage).  // completion: add level back
  Msg("Synced")

Delayed Animation

Use .After(d) to suppress the animation for an initial duration. If the task finishes before the delay, no animation is shown at all - useful for operations that are usually fast but occasionally slow:

err := clog.Spinner("Fetching config").
  After(time.Second).
  Wait(ctx, fetchConfig).
  Msg("Config loaded")

If fetchConfig completes in under 1 second, the user sees nothing until the final “Config loaded” message. If it takes longer, the spinner appears after 1 second.

Shimmer

Shimmer creates an independent animation where each character is coloured based on its position in a sweeping gradient wave.

Shimmer demo

// Default gradient
clog.Shimmer("Indexing documents").
  Wait(ctx, action).
  Msg("Indexed")

// Custom gradient with direction
clog.Shimmer("Synchronizing",
  clog.ColorStop{Position: 0, Color: colorful.Color{R: 0.3, G: 0.3, B: 0.8}},
  clog.ColorStop{Position: 0.5, Color: colorful.Color{R: 1, G: 1, B: 1}},
  clog.ColorStop{Position: 1, Color: colorful.Color{R: 0.3, G: 0.3, B: 0.8}},
).
  ShimmerDirection(clog.DirectionMiddleIn).
  Wait(ctx, action).
  Msg("Synchronized")

Use DefaultShimmerGradient() to get the default gradient stops.

Directions

ConstantDescription
DirectionRightLeft to right (default)
DirectionLeftRight to left
DirectionMiddleInInward from both edges
DirectionMiddleOutOutward from the center
DirectionBounceInInward from both edges, then bounces out
DirectionBounceOutOutward from center, then bounces in

Shimmer directions

Speed

Control how fast the animation cycles with Speed(cyclesPerSecond). The default is 0.5 (one full cycle every two seconds) for both Shimmer and Pulse. Values ≤ 0 are treated as the default.

clog.Shimmer("Fast shimmer").
  Speed(2.0).  // 2 gradient cycles per second
  Wait(ctx, action).
  Msg("Done")

clog.Pulse("Quick pulse").
  Speed(1.5).  // 1.5 oscillations per second
  Wait(ctx, action).
  Msg("Done")

Both pulse and shimmer use ColorStop for gradient definitions:

type ColorStop struct {
  Position float64        // 0.0-1.0
  Color    colorful.Color // from github.com/lucasb-eyer/go-colorful
}

Pulse

Pulse creates an independent animation where all characters in the message fade uniformly between gradient colours.

Pulse demo

// Default gradient (blue-gray to cyan)
clog.Pulse("Warming up").
  Wait(ctx, action).
  Msg("Ready")

// Custom gradient
clog.Pulse("Replicating",
  clog.ColorStop{Position: 0, Color: colorful.Color{R: 1, G: 0.2, B: 0.2}},
  clog.ColorStop{Position: 0.5, Color: colorful.Color{R: 1, G: 1, B: 0.3}},
  clog.ColorStop{Position: 1, Color: colorful.Color{R: 1, G: 0.2, B: 0.2}},
).
  Wait(ctx, action).
  Msg("Replicated")

Use DefaultPulseGradient() to get the default gradient stops.

Bar

Bar creates a determinate progress bar that shows filled/empty cells and a live percentage. Use SetProgress on the ProgressUpdate to advance the bar.

Bar demo

err := clog.Bar("Downloading", 100).
  Str("file", "release.tar.gz").
  Elapsed("elapsed").
  Progress(ctx, func(ctx context.Context, p *clog.ProgressUpdate) error {
    for i := range 101 {
      p.SetProgress(i).Msg("Downloading").Send()
      time.Sleep(20 * time.Millisecond)
    }
    return nil
  }).
  Prefix("✅").
  Msg("Download complete")
// INF ⏳ Downloading [━━━━━━━━╸───────────] 42% elapsed=1.2s
// INF ✅ Download complete file=release.tar.gz elapsed=3.4s

SetTotal can be called mid-task to update the denominator if the total becomes known after the task starts:

p.SetProgress(50).SetTotal(200).Msg("Processing").Send()

AddTotal atomically adds to the total - useful for “discovered more work” patterns where the full scope isn’t known upfront:

p.AddTotal(50)  // discovered 50 more items
p.AddTotal(-10) // 10 items turned out to be duplicates

Styles

Six pre-built styles are available in progress_bar_presets.go. Pass any of them to .Style():

PresetCharactersDescription
BarBasic[=====> ]ASCII-only for maximum compatibility
BarDash[----- ]Simple dash fill
BarThin[━━━╺──────]Box-drawing with half-cell resolution (default)
BarBlock│█████░░░░░│Solid block characters
BarGradient│██████▍ │Block elements with 8x sub-cell resolution
BarSmooth│████▌ │Block characters with half-block leading edge

Bar styles

clog.Bar("Uploading", total).
  Style(clog.BarSmooth).
  Progress(ctx, task).
  Msg("Done")

BarThin and BarSmooth use half-cell resolution via HalfFilled (and HalfEmpty for BarThin), giving twice the visual granularity of full-cell styles. BarGradient uses GradientFill for 8x sub-cell resolution - the smoothest built-in option.

Custom Style

Build a fully custom style by passing a BarStyle struct:

clog.Bar("Uploading", total).
  Style(clog.BarStyle{
    Align:       clog.BarAlignInline, // inline with message (default: BarAlignRightPad)

    CapStyle:    new(lipgloss.NewStyle().Bold(true)), // style for [ ] caps (default: bold white)
    CapLeft:     "|",
    CapRight:    "|",

    CharEmpty:   '-',
    CharFill:    '=',
    CharHead:    '>', // decorative head at leading edge (0 = disabled)

    HalfEmpty:   0, // half-cell trailing edge for 2x resolution (0 = disabled)
    HalfFilled:  0, // half-cell leading edge for 2x resolution (0 = disabled)

    Separator:   " ", // separator between message, bar, and widget text

    StyleEmpty:  new(lipgloss.NewStyle().Foreground(lipgloss.Color("8"))),  // grey
    StyleFill:   new(lipgloss.NewStyle().Foreground(lipgloss.Color("2"))),  // green

    WidgetLeft:  clog.WidgetPercent(clog.WithDigits(1)), // "50.0%" to the left of the bar
    WidgetRight: clog.WidgetNone, // suppress the default right-side percent

    Width:       30, // fixed inner width (0 = auto-size from terminal)
    WidthMin:    10, // auto-size minimum (default 10)
    WidthMax:    40, // auto-size maximum (default 40)
  }).
  Progress(ctx, task).
  Msg("Done")

When Width is 0, the bar auto-sizes to one quarter of the terminal width, clamped to [WidthMin, WidthMax].

All presets include bold white CapStyle for the bar caps. Set CapStyle to nil for unstyled caps.

Progress Gradient

Color the bar fill based on progress using ProgressGradient. The filled portion shifts through the gradient as progress advances (e.g. red at 0%, yellow at 50%, green at 100%):

style := clog.BarBlock
style.ProgressGradient = clog.DefaultBarGradient() // red → yellow → green

clog.Bar("Building", 100).
  Style(style).
  Progress(ctx, task).
  Msg("Built")

Custom gradients work the same as other gradient fields:

style.ProgressGradient = []clog.ColorStop{
  {Position: 0, Color: colorful.Color{R: 0.3, G: 0.3, B: 1}},   // blue
  {Position: 0.5, Color: colorful.Color{R: 1, G: 1, B: 1}},     // white
  {Position: 1, Color: colorful.Color{R: 0.3, G: 1, B: 0.3}},   // green
}

When set, ProgressGradient overrides the StyleFill foreground color. Use DefaultBarGradient() to get the default red → yellow → green stops.

Alignment

The Align field on BarStyle controls where the bar appears on the line:

ConstantLayout
BarAlignRightPadINF ⏳ Downloading [━━━━━╸╺──────] 45% (default)
BarAlignLeftPadINF ⏳ [━━━━━╸╺──────] 45% Downloading
BarAlignInlineINF ⏳ Downloading [━━━━━╸╺──────] 45%
BarAlignRightINF ⏳ Downloading [━━━━━╸╺──────] 45%
BarAlignLeftINF ⏳ [━━━━━╸╺──────] 45% Downloading

The padded variants (BarAlignRightPad, BarAlignLeftPad) fill the gap between message and bar with spaces to span the terminal width. When the terminal is too narrow, they fall back to the Separator between parts.

Widgets

WidgetLeft and WidgetRight on BarStyle control text annotations beside the bar. Each is a BarWidget - a callback that receives progress state and returns a string:

type BarState struct {
  Current int
  Total   int
  Elapsed time.Duration
  Rate    float64 // items per second
}
type BarWidget func(BarState) string

All presets set WidgetRight: WidgetPercent(0) - padded percentage on the right (e.g. " 42%"). When both widgets are nil, the same default applies.

Built-in widgets:

WidgetDescription
WidgetPercent(n)Padded percentage with n decimal places
WidgetBytes()SI byte progress (e.g. " 50 MB / 100 MB", base-1000)
WidgetIBytes()IEC byte progress (e.g. "50 MiB / 100 MiB", base-1024)
WidgetETA()Estimated time remaining (e.g. "ETA 2m30s", "ETA ∞")
WidgetRate()Items per second (e.g. "150/s", "1.5k/s")
WidgetBytesRate()SI byte throughput (e.g. "82.9 MB/s")
WidgetIBytesRate()IEC byte throughput (e.g. "82.9 MiB/s")
WidgetNoneAlways returns “” - suppresses default percent
WidgetSeparator(s)Always renders s - use as a divider inside Widgets

All widget constructors accept WidgetOption values:

OptionApplies toDescription
WithDigits(n)WidgetPercent, WidgetBytes, WidgetIBytes, WidgetBytesRate, WidgetIBytesRatePrecision: significant digits or decimal places
WithUnit(label)WidgetRateUnit label (e.g. "ops""150 ops/s")
WithStyle(s)all widgetsLipgloss style applied to the widget’s output

Composing multiple widgets:

Use Widgets to combine several widgets on a single side. Empty outputs are filtered and the rest are joined with a space:

style := clog.BarThin
style.WidgetRight = clog.Widgets(clog.WidgetETA(), clog.WidgetRate())
// INF ⏳ Processing [━━━━━╸╺──────] ETA 2m30s 150/s

Add a visual divider with WidgetSeparator:

style.WidgetRight = clog.Widgets(
  clog.WidgetETA(),
  clog.WidgetSeparator("│"),
  clog.WidgetRate(),
)
// INF ⏳ Processing [━━━━━╸╺──────] ETA 2m30s │ 150/s

Pass WithStyle to any widget to apply a Lipgloss style to its output:

faint := new(lipgloss.NewStyle().Faint(true))
style.WidgetRight = clog.Widgets(
  clog.WidgetETA(clog.WithStyle(faint)),
  clog.WidgetSeparator("│", clog.WithStyle(faint)),
  clog.WidgetRate(clog.WithStyle(faint)),
)
// INF ⏳ Processing [━━━━━╸╺──────] ETA 2m30s │ 150/s  (all rendered faint)

Move percent to the left:

style := clog.BarThin
style.WidgetLeft = clog.WidgetPercent(0)
style.WidgetRight = clog.WidgetNone

Download progress with byte sizes:

fileSize := 150 * 1000 * 1000 // 150 MB
style := clog.BarSmooth
style.WidgetRight = clog.WidgetBytes()

clog.Bar("Downloading", fileSize).
  Style(style).
  Str("file", "model.bin").
  Progress(ctx, func(ctx context.Context, p *clog.ProgressUpdate) error {
    // p.SetProgress(bytesReceived).Send()
  }).
  Msg("Downloaded")
// INF ⏳ Downloading │████▌     │  75 MB / 150 MB file=model.bin

The current value is right-aligned to the total’s width to prevent the bar from jumping as digits change. Use WidgetIBytes() for base-1024 units (KiB, MiB, GiB).

ETA and rate:

style := clog.BarBlock
style.WidgetLeft = clog.WidgetETA()
style.WidgetRight = clog.WidgetRate(clog.WithUnit("items"))

clog.Bar("Processing", 500).
  Style(style).
  Progress(ctx, task).
  Msg("Done")
// INF ⏳ Processing ETA 2m30s │█████░░░░░│ 150 items/s

WidgetETA shows "ETA ∞" before any progress, a countdown during the task, and "" when complete. WidgetRate accepts an optional WithUnit for a label; without it, output is "150/s".

Byte throughput:

style := clog.BarGradient
style.WidgetRight = clog.WidgetBytesRate()

clog.Bar("Uploading", totalBytes).
  Style(style).
  Progress(ctx, task).
  Msg("Done")
// INF ⏳ Uploading │██████▍   │ 82.9 MB/s

Use WidgetIBytesRate() for IEC units (MiB/s). Both accept WithDigits(n) to control significant digits (default 3).

Custom widget:

style := clog.BarThin
style.WidgetRight = func(s clog.BarState) string {
  remaining := s.Total - s.Current
  return fmt.Sprintf("%d remaining", remaining)
}

Suppress percent entirely:

style := clog.BarThin
style.WidgetRight = clog.WidgetNone

Percentage as a Field

Use .BarPercent(key) on the builder to move the percentage into structured fields. This suppresses the default right-side widget:

clog.Bar("Installing", 100).
  BarPercent("progress").
  Elapsed("elapsed").
  Progress(ctx, task).
  Msg("Installed")
// INF ⏳ Installing          [━━━━━╸╺──────] progress=45% elapsed=1.2s

All animations gracefully degrade: when colours are disabled (CI, piped output), a static status line with an ⏳ prefix is printed instead.

The icon displayed during Pulse, Shimmer, and Bar animations defaults to ⏳ and can be changed with .Prefix() on the builder:

clog.Pulse("Warming up").
  Prefix("🔄").
  Wait(ctx, action).
  Msg("Ready")

Group

Group runs multiple animations concurrently in a multi-line block, redrawn each tick.

Group demo

g := clog.NewGroup(ctx)
g.Add(clog.Spinner("Processing data").Int("workers", 4)).
  Run(func(ctx context.Context) error {
    return processData(ctx)
  })
g.Add(clog.Bar("Downloading", 100)).
  Progress(func(ctx context.Context, p *clog.ProgressUpdate) error {
    for i := range 101 {
      p.SetProgress(i).Send()
      time.Sleep(20 * time.Millisecond)
    }
    return nil
  })
g.Wait().Prefix("✅").Msg("All tasks complete")

While the tasks run, the terminal shows all animations updating simultaneously:

INF 🌒 Processing data workers=4
INF ⏳ Downloading [━━━━━━━╸╺───────────] 42%

When all tasks finish, the block is cleared and a single summary line is logged. Alternatively, use per-task results for individual completion messages:

g := clog.NewGroup(ctx)
proc := g.Add(clog.Spinner("Processing")).Run(processData)
dl := g.Add(clog.Bar("Downloading", 100)).Progress(download)
g.Wait()
proc.Prefix("✅").Msg("Processing done")
dl.Prefix("✅").Msg("Download complete")

Any mix of animation types works: spinners, bars, pulses, and shimmers can all run in the same group.

API

Function / MethodDescription
clog.NewGroup(ctx)Create a group using the Default logger
logger.Group(ctx)Create a group using a specific logger
g.Add(builder)Register an animation builder, returns *GroupEntry
entry.Run(task)Start a TaskFunc, returns *TaskResult
entry.Progress(task)Start a ProgressTaskFunc, returns *TaskResult
g.Wait()Block until all tasks complete, returns *GroupResult

GroupResult and TaskResult support the same chaining as WaitResult: .Msg(), .Parts(), .Prefix(), .Send(), .Err(), .Silent(), .OnErrorLevel(), .OnErrorMessage(), .OnSuccessLevel(), .OnSuccessMessage(), and all field methods (.Str(), .Int(), etc.).

Per-Event Parts Override

Override the part order for individual animations and their completion messages:

g := clog.NewGroup(ctx)

// Animation + completion both use overridden parts.
proc := g.Add(clog.Spinner("Processing").Parts(clog.PartMessage)).
  Run(processData)

dl := g.Add(clog.Bar("Downloading", 100)).
  Progress(download)

// Override parts only on the summary line.
g.Wait().Parts(clog.PartPrefix, clog.PartMessage).Msg("All done")

.Parts() on the AnimationBuilder propagates to the TaskResult. Call .Parts() on TaskResult or GroupResult to override independently for the completion message.

GroupResult.Err() / .Silent() returns the errors.Join of all task errors (nil when all succeeded).

Styles

Customise the visual appearance using lipgloss styles:

Styled output

styles := clog.DefaultStyles()

// Customise level colours
styles.Levels[clog.ErrorLevel] = new(
  lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9")), // bright red
)

// Customise field key appearance
styles.KeyDefault = new(
  lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")), // bright blue
)

clog.SetStyles(styles)

Value Colouring

Values are styled with a three-tier priority system:

  1. Key styles - style all values of a specific field key
  2. Value styles - style values matching a typed key (bool true != string "true")
  3. Type styles - style values by their Go type
styles := clog.DefaultStyles()

// 1. Key styles: all values of the "status" field are green
styles.Keys["status"] = new(lipgloss.NewStyle().
  Foreground(lipgloss.Color("2"))) // green

// 2. Value styles: typed key matches (bool `true` != string "true")
styles.Values["PASS"] = new(
  lipgloss.NewStyle().
  Foreground(lipgloss.Color("2")), // green
)

styles.Values["FAIL"] = new(lipgloss.NewStyle().
  Foreground(lipgloss.Color("1")), // red
)

// 3. Type styles: string values -> white, numeric values -> magenta, errors -> red by default
styles.FieldString = new(lipgloss.NewStyle().Foreground(lipgloss.Color("15")))
styles.FieldNumber = new(lipgloss.NewStyle().Foreground(lipgloss.Color("5")))
styles.FieldError  = new(lipgloss.NewStyle().Foreground(lipgloss.Color("1")))
styles.FieldString = nil  // set to nil to disable
styles.FieldNumber = nil  // set to nil to disable

clog.SetStyles(styles)

Styles Reference

FieldTypeAliasDefault
DurationThresholdsmap[string][]ThresholdThresholdMap{}
DurationUnitsmap[string]StyleStyleMap{}
FieldDurationNumberStylemagenta
FieldDurationUnitStylemagenta faint
FieldElapsedNumberStylenil (→ DurationNumber)
FieldElapsedUnitStylenil (→ DurationUnit)
FieldErrorStylered
FieldJSON*JSONStylesDefaultJSONStyles()
FieldNumberStylemagenta
FieldPercentStylenil
FieldQuantityNumberStylemagenta
FieldQuantityUnitStylemagenta faint
FieldStringStylewhite
FieldTimeStylemagenta
KeyDefaultStyleblue
Keysmap[string]StyleStyleMap{}
Levelsmap[Level]StyleLevelStyleMapper-level bold colours
Messagesmap[Level]StyleLevelStyleMapDefaultMessageStyles()
Prefixesmap[Level]StyleLevelStyleMap{}
PercentGradient[]ColorStopred → yellow → green
QuantityThresholdsmap[string][]ThresholdThresholdMap{}
QuantityUnitsmap[string]StyleStyleMap{}
SeparatorStylefaint
TimestampStylefaint
Valuesmap[any]StyleValueStyleMapDefaultValueStyles()
FieldDescription
DurationThresholdsDuration unit -> magnitude-based style thresholds
DurationUnitsDuration unit string -> style override
FieldDurationNumberStyle for numeric segments of duration values (e.g. “1” in “1m30s”), nil to disable
FieldDurationUnitStyle for unit segments of duration values (e.g. “m” in “1m30s”), nil to disable
FieldElapsedNumberStyle for numeric segments of elapsed-time values; nil falls back to FieldDurationNumber
FieldElapsedUnitStyle for unit segments of elapsed-time values; nil falls back to FieldDurationUnit
FieldErrorStyle for error field values, nil to disable
FieldJSONPer-token styles for JSON syntax highlighting; nil disables highlighting
FieldNumberStyle for int/float field values, nil to disable
FieldPercentBase style for Percent fields (foreground overridden by gradient), nil to disable
FieldQuantityNumberStyle for numeric part of quantity values (e.g. “5” in “5km”), nil to disable
FieldQuantityUnitStyle for unit part of quantity values (e.g. “km” in “5km”), nil to disable
FieldStringStyle for string field values, nil to disable
FieldTimeStyle for time.Time field values, nil to disable
KeyDefaultStyle for field key names without a per-key override, nil to disable
KeysField key name -> value style override
LevelsPer-level label style (e.g. “INF”, “ERR”), nil to disable
MessagesPer-level message text style, nil to disable
PrefixesPer-level prefix style
PercentGradientGradient colour stops for Percent fields
QuantityThresholdsQuantity unit -> magnitude-based style thresholds
QuantityUnitsQuantity unit string -> style override
SeparatorStyle for the separator between key and value
TimestampStyle for the timestamp prefix, nil to disable
ValuesTyped value -> style (uses Go equality, so bool true != string "true")

Configuration

Behavioural settings are configured via setter methods on Logger (or package-level convenience functions for the Default logger):

SetterTypeDefaultDescription
SetAnimationIntervaltime.Duration67msMinimum refresh interval for all animations (0 = use built-in rates)
SetElapsedFormatFuncfunc(time.Duration) stringnilCustom format function for Elapsed fields
SetElapsedMinimumtime.Durationtime.SecondMinimum duration for Elapsed fields to be displayed
SetElapsedPrecisionint0Decimal places for Elapsed display (0 = “3s”, 1 = “3.2s”)
SetElapsedRoundtime.Durationtime.SecondRounding granularity for Elapsed values (0 to disable)
SetFieldSortSortSortNoneSort order: SortNone, SortAscending, SortDescending
SetPercentFormatFuncfunc(float64) stringnilCustom format function for Percent fields
SetPercentReverseGradientboolfalseReverse the percent gradient (green=0%, red=100%) for this logger
SetPercentPrecisionint0Decimal places for Percent display (0 = “75%”, 1 = “75.0%”)
SetQuantityUnitsIgnoreCasebooltrueCase-insensitive quantity unit matching
SetSeparatorTextstring"="Key/value separator string

Each Threshold pairs a minimum value with style overrides:

type ThresholdStyle struct {
  Number Style // Override for the number segment (nil = keep default).
  Unit   Style // Override for the unit segment (nil = keep default).
}

type Threshold struct {
  Value float64        // Minimum numeric value (inclusive) to trigger this style.
  Style ThresholdStyle // Style overrides for number and unit segments.
}

Thresholds are evaluated in descending order - the first match wins:

styles.QuantityThresholds["ms"] = clog.Thresholds{
  {Value: 5000, Style: clog.ThresholdStyle{Number: redStyle, Unit: redStyle}},
  {Value: 1000, Style: clog.ThresholdStyle{Number: yellowStyle, Unit: yellowStyle}},
}

Value styles only apply at Info level and above by default. Use SetFieldStyleLevel to change the threshold.

Per-Level Message Styles

Style the log message text differently for each level:

styles := clog.DefaultStyles()

styles.Messages[clog.ErrorLevel] = new(
  lipgloss.NewStyle().Foreground(lipgloss.Color("1")), // red
)

styles.Messages[clog.WarnLevel] = new(
  lipgloss.NewStyle().Foreground(lipgloss.Color("3")), // yellow
)

clog.SetStyles(styles)

Use DefaultMessageStyles() to get the defaults (unstyled for all levels).

Use DefaultValueStyles() to get the default value styles (true=green, false=red, nil=grey, ""=grey).

Use DefaultPercentGradient() to get the default red → yellow → green gradient stops used for Percent fields.

Percent Gradient Direction

By default the Percent gradient runs red (0%) → yellow (50%) → green (100%) - useful when a higher value is better (e.g. battery, health score). For metrics where a lower value is better (CPU usage, disk usage, error rate), reverse the gradient:

// Global: all Percent fields on the Default logger
clog.SetPercentReverseGradient(true)

// Per-logger: all Percent fields on this logger
logger.SetPercentReverseGradient(true)

// Per-field: just this Percent field, regardless of logger setting
clog.Info().
  Percent("cpu", 92, clog.WithPercentReverseGradient()).
  Percent("battery", 85).
  Msg("System status")
// "cpu" renders red at 92%, "battery" renders green at 85%

WithPercentReverseGradient() is a PercentOption passed directly to Event.Percent. It toggles the logger-level setting for that field - so if the logger already has the gradient reversed, WithPercentReverseGradient() flips it back to normal. This makes it easy to mix metrics with different semantics on the same log line regardless of the logger default.

Format Hooks

Override the default formatting for Elapsed and Percent fields:

// Custom elapsed format: truncate to whole seconds
clog.SetElapsedFormatFunc(func(d time.Duration) string {
  return d.Truncate(time.Second).String()
})

// Custom percent format: "75/100" instead of "75%"
clog.SetPercentFormatFunc(func(v float64) string {
  return fmt.Sprintf("%.0f/100", v)
})

When set to nil (the default), the built-in formatters are used (formatElapsed with SetElapsedPrecision for elapsed, strconv.FormatFloat with SetPercentPrecision + “%” for percent).

Field Sort Order

Control the order fields appear in log output. By default fields preserve insertion order.

// Sort fields alphabetically by key
clog.SetFieldSort(clog.SortAscending)

// Or reverse alphabetical
clog.SetFieldSort(clog.SortDescending)
ConstantDescription
SortNonePreserve insertion order (default)
SortAscendingSort fields by key A→Z
SortDescendingSort fields by key Z→A
clog.Info().
  Str("zoo", "animals").
  Str("alpha", "first").
  Int("count", 42).
  Msg("Sorted")
// SortNone:       INF ℹ️ Sorted zoo=animals alpha=first count=42
// SortAscending:  INF ℹ️ Sorted alpha=first count=42 zoo=animals
// SortDescending: INF ℹ️ Sorted zoo=animals count=42 alpha=first

Renderer Binding

Styles created with lipgloss.NewStyle() are bound to the global renderer (anchored to os.Stdout). When stdout is piped, that renderer detects Ascii profile and all colours are silently stripped - even for styles meant for stderr.

clog handles this automatically: New, SetStyles, SetOutput, and SetColorMode all call WithRenderer to rebind styles to the logger’s output renderer. Custom styles passed to SetStyles are transparently rebound - no changes needed in user code.

For advanced use, Styles and JSONStyles expose WithRenderer(*lipgloss.Renderer):

styles := clog.DefaultStyles()
styles.WithRenderer(output.Renderer())

Both methods mutate and return the receiver for fluent chaining.

Hyperlinks

Render clickable terminal hyperlinks using OSC 8 escape sequences:

// Typed field methods (recommended)
clog.Info().Path("dir", "src/").Msg("Directory")
clog.Info().Line("file", "config.yaml", 42).Msg("File with line")
clog.Info().Column("loc", "main.go", 42, 10).Msg("File with line and column")
clog.Info().URL("docs", "https://example.com/docs").Msg("See docs")
clog.Info().Link("docs", "https://example.com", "docs").Msg("URL")

// Standalone functions (for use with Str)
link := clog.PathLink("config.yaml", 42)               // file path with line number
link := clog.PathLink("src/", 0)                       // directory (no line number)
link := clog.Hyperlink("https://example.com", "docs")  // arbitrary URL

IDE Integration

Configure hyperlinks to open files directly in your editor:

// Generic fallback for any path (file or directory)
clog.SetHyperlinkPathFormat("vscode://file{path}")

// File-specific (overrides path format for files)
clog.SetHyperlinkFileFormat("vscode://file{path}")

// Directory-specific (overrides path format for directories)
clog.SetHyperlinkDirFormat("finder://{path}")

// File+line hyperlinks (Line, PathLink with line > 0)
clog.SetHyperlinkLineFormat("vscode://file{path}:{line}")
clog.SetHyperlinkLineFormat("idea://open?file={path}&line={line}")

// File+line+column hyperlinks (Column)
clog.SetHyperlinkColumnFormat("vscode://file{path}:{line}:{column}")

Use {path}, {line}, and {column} (or {col}) as placeholders. Default format is file://{path}.

Format resolution order:

ContextFallback chain
DirectoryDirFormat -> PathFormat -> file://{path}
File (no line)FileFormat -> PathFormat -> file://{path}
File + lineLineFormat -> file://{path}
File + columnColumnFormat -> LineFormat -> file://{path}

These can also be set via environment variables:

export CLOG_HYPERLINK_FORMAT="vscode"                      # named preset (sets all slots)
export CLOG_HYPERLINK_PATH_FORMAT="vscode://{path}"        # generic fallback
export CLOG_HYPERLINK_FILE_FORMAT="vscode://file{path}"    # files only
export CLOG_HYPERLINK_DIR_FORMAT="finder://{path}"         # directories only
export CLOG_HYPERLINK_LINE_FORMAT="vscode://{path}:{line}"
export CLOG_HYPERLINK_COLUMN_FORMAT="vscode://{path}:{line}:{column}"

CLOG_HYPERLINK_FORMAT accepts a preset name and configures all slots at once. Individual format vars override the preset for their specific slot.

Named Presets

clog.SetHyperlinkPreset("vscode") // or CLOG_HYPERLINK_FORMAT=vscode
PresetScheme
cursorcursor://
kittyfile:// with #line
macvimmvim://
sublsubl://
textmatetxmt://
vscodevscode://
vscode-insidersvscode-insiders://
vscodiumvscodium://

Hyperlinks are automatically disabled when colours are disabled.

JSON

JSON marshals any Go value to JSON; RawJSON accepts pre-serialized bytes. Both emit the result with syntax highlighting.

JSON highlighting

// Marshal a Go value
clog.Info().JSON("user", userStruct).Msg("ok")
clog.Info().JSON("config", map[string]any{"port": 8080, "debug": true}).Msg("started")

// Pre-serialized bytes (no marshal overhead)
clog.Error().
  Str("batch", "1/1").
  RawJSON("error", []byte(`{"status":"unprocessable_entity","detail":"validation failed","code":null}`)).
  Msg("Batch failed")
// ERR ❌ Batch failed batch=1/1 error={"status":"unprocessable_entity","detail":"validation failed","code":null}

Use JSON when you have a Go value to log; use RawJSON when you already have bytes (HTTP response bodies, json.RawMessage, database JSON columns) to avoid an unnecessary marshal/unmarshal round-trip. JSON logs the error string as the field value if marshalling fails.

Pretty-printed JSON is automatically flattened to a single line. Highlighting uses a Dracula-inspired colour scheme by default (space after commas included). Disable or customise it via FieldJSON in Styles:

// Disable highlighting
styles := clog.DefaultStyles()
styles.FieldJSON = nil
clog.SetStyles(styles)

// Custom colours
custom := clog.DefaultJSONStyles()
custom.Key = new(lipgloss.NewStyle().Foreground(lipgloss.Color("#50fa7b")))
styles.FieldJSON = custom
clog.SetStyles(styles)

Number is the base fallback for all numeric tokens. Five sub-styles allow finer control and fall back to Number when nil:

FieldApplies to
NumberPositivePositive numbers (with or without explicit +)
NumberNegativeNegative numbers
NumberZeroZero (falls back to NumberPositive, then Number)
NumberFloatFloating-point values
NumberIntegerInteger values
custom := clog.DefaultJSONStyles()
custom.NumberNegative = new(lipgloss.NewStyle().Foreground(lipgloss.Color("1"))) // red
custom.NumberZero = new(lipgloss.NewStyle().Foreground(lipgloss.Color("8")))     // grey
styles.FieldJSON = custom
clog.SetStyles(styles)

Rendering Modes

Set JSONStyles.Mode to control how JSON structure is rendered:

ModeDescriptionExample
JSONModeJSONStandard JSON (default){"status":"ok","count":42}
JSONModeHumanUnquote keys and simple string values{status:ok, count:42}
JSONModeFlatFlatten nested object keys with dot notation; arrays kept intact{status:ok, meta.region:us-east-1}

JSONModeHuman - keys are unquoted unless they contain ,{}[]\s:#"' or start with ////*. String values are unquoted unless they start with a forbidden character, end with whitespace, are ambiguous as a JSON keyword (true, false, null), or look like a number. Empty strings always render as "".

styles.FieldJSON = clog.DefaultJSONStyles()
styles.FieldJSON.Mode = clog.JSONModeHuman

clog.Info().
  RawJSON("response", []byte(`{"status":"ok","count":42,"active":true,"deleted_at":null}`)).
  Msg("Fetched")
// INF ℹ️ Fetched response={status:ok, count:42, active:true, deleted_at:null}

JSONModeFlat - nested objects are recursed into and their keys joined with .; arrays are kept intact as values:

styles.FieldJSON.Mode = clog.JSONModeFlat

clog.Info().
  RawJSON("resp", []byte(`{"user":{"name":"alice","role":"admin"},"tags":["a","b"]}`)).
  Msg("Auth")
// INF ℹ️ Auth resp={user.name:alice, user.role:admin, tags:[a, b]}

Spacing

JSONStyles.Spacing is a bitmask controlling where spaces are inserted. The default (DefaultJSONStyles) adds a space after commas.

FlagEffectExample
JSONSpacingAfterColonSpace after :{"key": "value"}
JSONSpacingAfterCommaSpace after ,{"a":1, "b":2}
JSONSpacingBeforeObjectSpace before a nested {{"key": {"n":1}}
JSONSpacingBeforeArraySpace before a nested [{"tags": ["a","b"]}
JSONSpacingAllAll of the above{"key": {"n": 1}, "tags": ["a"]}
// Fluent builder
styles.FieldJSON = clog.DefaultJSONStyles().WithSpacing(clog.JSONSpacingAll)

// Direct assignment
styles.FieldJSON.Spacing = clog.JSONSpacingAfterComma | clog.JSONSpacingBeforeObject

JSONSpacingAfterColon and JSONSpacingBeforeObject/JSONSpacingBeforeArray are independent - combining them produces two spaces before a nested value.

Omitting Commas

Set OmitCommas: true to drop the , separator. Combine with JSONSpacingAfterComma to keep a space in its place:

styles.FieldJSON.OmitCommas = true
styles.FieldJSON.Spacing |= clog.JSONSpacingAfterComma

clog.Info().
  RawJSON("r", []byte(`{"a":1,"b":2,"c":true}`)).
  Msg("ok")
// INF ℹ️ ok r={a:1 b:2 c:true}

Handlers

Implement the Handler interface for custom output formats:

type Handler interface {
  Log(Entry)
}

The Entry struct provides Level, Time, Message, Prefix, and Fields. The logger handles level filtering, field accumulation, timestamps, and locking - the handler only formats and writes.

// Using HandlerFunc adapter
clog.SetHandler(clog.HandlerFunc(func(e clog.Entry) {
  data, _ := json.Marshal(e)
  fmt.Println(string(data))
}))

Example output:

{"fields":[{"key":"port","value":"8080"}],"level":"info","message":"Server started"}

Level serializes as a human-readable string (e.g. "info", "error"). Time is omitted when timestamps are disabled. Fields and Prefix are omitted when empty.

log/slog Integration

Use NewSlogHandler to create a slog.Handler backed by a clog logger. This lets any code that accepts slog.Handler or *slog.Logger produce clog-formatted output.

h := clog.NewSlogHandler(clog.Default, nil)
logger := slog.New(h)

logger.Info("request handled", "method", "GET", "status", 200)
// INF ℹ️ request handled method=GET status=200

Options

h := clog.NewSlogHandler(clog.Default, &clog.SlogOptions{
  AddSource: true,           // include source file:line in each entry
  Level:     slog.LevelWarn, // override minimum level (nil = use logger's level)
})

Level Mapping

slog levelclog level
< LevelDebugTraceLevel
LevelDebugDebugLevel
LevelInfoInfoLevel
LevelWarnWarnLevel
LevelErrorErrorLevel
> LevelErrorFatalLevel

Records mapped to FatalLevel are logged but do not call os.Exit - only clog’s own Fatal().Msg() does that.

Groups and Attrs

WithGroup and WithAttrs work as expected. Groups use dot-notation keys matching clog’s Dict() pattern:

h := clog.NewSlogHandler(clog.Default, nil)
logger := slog.New(h).WithGroup("req")

logger.Info("handled", "method", "GET", "status", 200)
// INF ℹ️ handled req.method=GET req.status=200

NO_COLOR

clog respects the NO_COLOR convention. When the NO_COLOR environment variable is set (any value, including empty), all colours and hyperlinks are disabled.

Colour Control

Colour behaviour is set per-Output via ColorMode:

// Package-level (recreates Default logger's Output)
clog.SetColorMode(clog.ColorAlways) // force colours (overrides NO_COLOR)
clog.SetColorMode(clog.ColorNever)  // disable all colours and hyperlinks
clog.SetColorMode(clog.ColorAuto)   // detect terminal capabilities (default)

// Per-logger via Output
logger := clog.New(clog.NewOutput(os.Stdout, clog.ColorAlways))

This is useful in tests to verify hyperlink output without mutating global state:

l := clog.New(clog.NewOutput(&buf, clog.ColorAlways))
l.Info().Line("file", "main.go", 42).Msg("Loaded")
// buf contains OSC 8 hyperlink escape sequences

ColorMode implements encoding.TextMarshaler and encoding.TextUnmarshaler, so it works directly with flag.TextVar and most flag libraries.

Piped stdout

When stdout is piped (non-TTY) but stderr is a terminal, styles created with lipgloss.NewStyle() normally lose their colours because lipgloss binds them to the global renderer (anchored to os.Stdout).

clog handles this transparently: all styles are automatically rebound to the logger’s output renderer via WithRenderer. This means colours work correctly on stderr even when stdout is redirected:

// Styles retain colours on stderr even when stdout is piped.
logger := clog.New(clog.Stderr(clog.ColorAuto))
logger.Info().Str("status", "ok").Msg("Ready")

For advanced use, Styles.WithRenderer and JSONStyles.WithRenderer are available to manually rebind styles to a specific *lipgloss.Renderer:

styles := clog.DefaultStyles()
styles.WithRenderer(output.Renderer())

Configuration

Default Logger

The package-level functions (Info(), Warn(), etc.) use the Default logger which writes to os.Stdout at InfoLevel.

// Full configuration
clog.Configure(&clog.Config{
  Verbose: true,                            // enables debug level + timestamps
  Output:  clog.Stderr(clog.ColorAuto),     // custom output
  Styles:  customStyles,                    // custom visual styles
})

// Toggle verbose mode
clog.SetVerbose(true)

Output

Each Logger writes to an *Output, which bundles an io.Writer with its terminal capabilities (TTY detection, width, color profile):

// Standard constructors
out := clog.Stdout(clog.ColorAuto)                  // os.Stdout with auto-detection
out := clog.Stderr(clog.ColorAlways)                // os.Stderr with forced colours
out := clog.NewOutput(w, clog.ColorNever)           // arbitrary writer, colours disabled
out := clog.TestOutput(&buf)                        // shorthand for NewOutput(w, ColorNever)

Output methods:

MethodDescription
Writer()Returns the underlying io.Writer
IsTTY()True if the writer is connected to a terminal
ColorsDisabled()True if colours are suppressed for this output
Width()Terminal width (0 for non-TTY, lazily cached)
RefreshWidth()Re-detect terminal width on next Width() call
Renderer()Returns the lipgloss renderer

Custom Logger

logger := clog.New(clog.Stderr(clog.ColorAuto))
logger.SetLevel(clog.DebugLevel)
logger.SetReportTimestamp(true)
logger.SetTimeFormat("15:04:05.000")
logger.SetFieldTimeFormat(time.Kitchen)    // format for .Time() fields (default: time.RFC3339)
logger.SetTimeLocation(time.UTC)           // timezone for timestamps (default: time.Local)
logger.SetFieldStyleLevel(clog.TraceLevel) // min level for field value styling (default: InfoLevel)
logger.SetHandler(myHandler)

For simple cases where you just need a writer with default color detection:

logger := clog.NewWriter(os.Stderr) // equivalent to New(NewOutput(os.Stderr, ColorAuto))

Utility Functions

clog.GetLevel()                  // returns the current level of the Default logger
clog.IsVerbose()                 // true if level is Debug or Trace
clog.IsTerminal()                // true if Default output is a terminal
clog.ColorsDisabled()            // true if colours are disabled on the Default logger
clog.SetOutput(out)              // change the output (accepts *Output)
clog.SetOutputWriter(w)          // change the output writer (with ColorAuto)
clog.SetExitFunc(fn)             // override os.Exit for Fatal (useful in tests)
clog.SetHyperlinksEnabled(false) // disable all hyperlink rendering
logger.Output()                  // returns the Logger's *Output

Environment Variables

All env vars follow the pattern {PREFIX}_{SUFFIX}. The default prefix is CLOG.

SuffixDefault env var
LOG_LEVELCLOG_LOG_LEVEL
HYPERLINK_FORMATCLOG_HYPERLINK_FORMAT
HYPERLINK_PATH_FORMATCLOG_HYPERLINK_PATH_FORMAT
HYPERLINK_FILE_FORMATCLOG_HYPERLINK_FILE_FORMAT
HYPERLINK_DIR_FORMATCLOG_HYPERLINK_DIR_FORMAT
HYPERLINK_LINE_FORMATCLOG_HYPERLINK_LINE_FORMAT
HYPERLINK_COLUMN_FORMATCLOG_HYPERLINK_COLUMN_FORMAT
CLOG_LOG_LEVEL=debug ./some-app  # enables debug logging + timestamps
CLOG_LOG_LEVEL=warn ./some-app   # suppresses info messages

Custom Env Prefix

Use SetEnvPrefix to whitelabel the env var names for your application. The custom prefix is checked first, with CLOG_ as a fallback.

clog.SetEnvPrefix("MYAPP")
// Now checks MYAPP_LOG_LEVEL first, then CLOG_LOG_LEVEL
// Now checks MYAPP_HYPERLINK_PATH_FORMAT first, then CLOG_HYPERLINK_PATH_FORMAT
// etc.

This means CLOG_LOG_LEVEL=debug always works as a universal escape hatch, even when the application uses a custom prefix.

NO_COLOR is never prefixed - it follows the no-color.org standard independently.