Skip to content

Logging

maxlandon edited this page Jul 18, 2026 · 1 revision

Logging · [User+Dev]

The public github.com/reeflective/team/log package is the console logging system shared by teamclients and teamservers. It is built entirely on the standard library's log/slog — no third-party logging dependency. Applications embedding a team core can reuse and restyle the exact same logging the core uses, or replace it wholesale with their own slog.Handler.


How the core logs by default

When you don't configure logging, server.New / client.New build a log.Logger that fans out every record to two destinations, each with its own independently adjustable level:

  • Console — colored, aligned, human-readable. Info/Debug/Trace go to stdout, Warn/Error/Fatal/Panic go to stderr. Default console level: Warn.
  • File — plain text (no color), with source [file:line]. Default file level: the server config's Log.Level (Info by default) on the server; Info on the client. Default paths: ~/.<app>/teamserver/logs/<app>.teamserver.log and ~/.<app>/teamclient/logs/<app>.teamclient.log.

The console/stderr split means normal operation stays quiet on stdout while warnings and errors surface immediately, and the file keeps a fuller record.


Choosing a format (console / text / json)

The console/stdout stream can be rendered in three formats (the file logger always stays plain text):

log.Format What it is Use for
log.FormatConsole (default) aligned, colored, human-readable interactive use
log.FormatText slog TextHandler, uncolored key=value grep/awk, log shippers
log.FormatJSON slog JSONHandler, structured records Loki / ELK / CloudWatch

From the CLI — the --log-format persistent flag on both trees:

myapp teamserver daemon --log-format json
myapp teamserver client users --log-format text

From code — the option, or at runtime:

teamserver, _ := server.New("myapp", server.WithLogFormat(log.FormatJSON))
// or later:
teamserver.SetLogFormat(log.FormatText)

Helpers: log.Formats() (stable-ordered list, drives completion), Format.Describe(), Format.Valid(), Format.String().


Restyling the console (keep the core loggers)

Use WithConsoleOptions when you want the team look with small changes — different level markers, colors, column widths, timestamp — while keeping the default console+file loggers and their runtime level control. The callback receives *log.ConsoleOptions pre-filled with the library defaults; tweak only what you want.

import "github.com/reeflective/team/log"

teamserver, _ := server.New("myapp",
    server.WithConsoleOptions(func(o *log.ConsoleOptions) {
        // Terse bracket markers instead of the default words.
        o.Levels = map[slog.Level]log.LevelStyle{
            slog.LevelInfo:  {Label: "[*]", Color: "blue"},
            slog.LevelWarn:  {Label: "[!]", Color: "yellow"},
            slog.LevelError: {Label: "[x]", Color: "red"},
        } // missing levels fall back to DefaultLevelStyles()

        o.ShowTimestamp = true
        o.TimestampFormat = "15:04:05.000"
        o.PackageWidth = 14      // widen the package column
        o.MessageColor = "bright-white"
    }),
)

ConsoleOptions is fully documented in the package; the restyleable fields are Levels, PackageWidth, LevelWidth, PackageColor, TimeColor, MessageColor, DisableColors, ShowTimestamp, TimestampFormat, AddSource. Colors are carapace/style names. Copy log.DefaultLevelStyles() and edit the entries you care about.

Both server and client expose WithConsoleOptions.


Injecting your own slog.Handler (replace the backend)

Use WithLogger when you already have a fully set-up slog.Handler (multiple destinations, your house format, a log shipper...). It becomes the core's sole logging backend, in place of the console+file split.

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})

teamserver, _ := server.New("myapp", server.WithLogger(handler))
teamclient, _ := client.New("myapp", client.WithLogger(handler), client.WithDialer(d))

Because the handler fully controls formatting and routing, the core's runtime level/output knobs do not apply to it: SetLogLevel, SetLogWriter and SetLogFormat become no-ops for a custom-handler logger. (On the server, the independent audit log is unaffected.)

Pick your tool:

Goal Use
Team look, minor tweaks WithConsoleOptions
Team look, different stream format WithLogFormat
Completely your own logging backend WithLogger

Logging in your own code with the team style

You don't have to embed a core to use the look. The package gives standalone constructors:

import "github.com/reeflective/team/log"

// A ready *slog.Logger with the team console style.
logger := log.NewConsole(log.ConsoleOptions{Level: someLevelVar, ShowTimestamp: true})

// Tag it so the aligned package column renders.
logger = log.Named(logger, "compiler", "build")
logger.Info("starting build", "target", "linux/amd64")

Other constructors:

  • log.NewStdio(level) — console-only logger (stdout/stderr split), no file.
  • log.New(logFile, stdioLevel, fileLevel, style) — the full console+file logger the core uses; logFile is any io.Writer (nil ⇒ console only).
  • log.NewFromHandler(handler) — wrap an existing slog.Handler.
  • log.NewJSON(w, level) — JSON logger to a writer.
  • log.NewFormatHandler(format, w, level, style) — a standalone handler for a chosen format.
  • log.NewConsoleHandler(opts) — the raw ConsoleHandler (an slog.Handler).

Named loggers and the package column

The ConsoleHandler renders lines of the form:

[HH:MM:SS] <level-marker> <package> <message> [key=value ...]

The <package> column is populated from two special attributes:

  • log.PackageKey ("teamserver_pkg") — the package/domain.
  • log.StreamKey ("stream") — a finer flow/stream (kept by JSON/audit handlers, not shown on the console text).

Named(logger, pkg, stream) is sugar that attaches both. On a core:

log := teamserver.NamedLogger("transport", "mTLS")
log.Info("serving gRPC teamserver", "addr", ln.Addr())

Runtime controls: level and output

On the cores (no-ops for a custom-handler logger):

teamserver.SetLogLevel(int(slog.LevelDebug)) // adjust console + file levels together
teamserver.SetLogFormat(log.FormatText)

teamclient.SetLogLevel(int(slog.LevelDebug))
teamclient.SetLogWriter(cmd.OutOrStdout(), cmd.ErrOrStderr()) // redirect console streams

SetLogWriter is how the generated command runners point the console at a cobra command's own stdout/stderr; it uses an internal swap-writer so streams can be redirected without rebuilding handlers.

From the CLI, the -v/--verbosity count flag raises verbosity (e.g. -vvv) and --log-format picks the stream format. See CLI Reference.


Custom levels

slog only defines Debug/Info/Warn/Error. The package adds three:

log.LevelTrace = slog.LevelDebug - 4 // -8   (below Debug)
log.LevelFatal = slog.LevelError + 4 // 12   (aborts the program)
log.LevelPanic = slog.LevelError + 8 // 16

Helpers: log.Trace(logger, msg, args...) logs at Trace; log.Fatal(logger, msg, args...) logs at Fatal and exits with status 1 (reserved for unrecoverable failures such as the certificate infrastructure). log.LevelFrom(int) clamps an int to [Trace, Panic].


The audit log

The teamserver keeps a separate JSON audit log (default ~/.<app>/teamserver/logs/audit.json), independent of the console/file loggers and their level knobs. It logs at Debug so every request is recorded. Transport middleware is where you use it:

audit, err := teamserver.AuditLogger() // *slog.Logger writing JSON to audit.json
audit.Info("request", "method", info.FullMethod, "user", user.Name)

log.NewAudit(w) builds one over any writer if you want your own destination.


In-memory logging

Because the core's filesystem is an abstracted (afero-backed) FS returned by Server.Filesystem() / Client.Filesystem(), a core running with WithInMemory() keeps its log files in memory. If you need your own ephemeral logger against that same FS, open a file on it and pass the writer to log.New(...). See Testing & In-Memory Use.


Related: Configuration → · Writing a Transport →

Clone this wiki locally