Skip to content

For TypeScript Developers ​

The Go port keeps the same mental model as the TypeScript original: a fluent API on top of any underlying logger, with persistent fields, per-call metadata, and errors as first-class concepts. If you're already using loglayer in a TS service, the Go API will feel familiar.

This page covers the deliberate conventions and naming differences so you don't have to discover them by trial and error.

API mapping ​

Methods are PascalCase in Go (per language convention) and camelCase in TypeScript:

TypeScriptGoNotes
new LogLayer({ ... })loglayer.New(loglayer.Config{ ... })Function, not a class
log.withContext({ ... })log.WithFields(loglayer.Fields{ ... })Renamed (see below)
log.withMetadata({ ... })log.WithMetadata(loglayer.Metadata{ ... })Same shape
log.withError(err)log.WithError(err)Same shape
log.withPrefix(s)log.WithPrefix(s)Same shape; returns a new logger
log.child()log.Child()Same shape
log.info('msg')log.Info("msg")Variadic ...any like TS varargs
log.errorOnly(err) / metadataOnlylog.ErrorOnly(err) / MetadataOnlySame shape
log.disableLogging()log.DisableLogging()Safe to call from any goroutine
MockLogLayerloglayer.NewMock()Returns the concrete *LogLayer

Why Context → Fields ​

The TS library calls the persistent key/value bag context (withContext, contextFieldName, IContextManager). In Go, context is a stdlib package (context.Context) used pervasively for request scoping, cancellation, and deadlines. Calling our concept Context would mean every reader has to figure out which context.

So Go uses Fields:

  • withContext → WithFields
  • clearContext → WithoutFields
  • muteContext → MuteFields
  • contextFieldName → FieldsKey
  • The type alias loglayer.Fields is map[string]any, same as TS's loose object shape.

The behavior is identical; only the name changed.

Per-call context.Context (the Go stdlib type, e.g. for trace IDs and deadlines) is attached separately via WithContext:

go
log.WithContext(ctx).Info("request received")

This concept doesn't exist in TS, since JavaScript doesn't have a comparable per-request context primitive.

Constructor and error handling ​

TypeScript:

ts
const log = new LogLayer({
  transport: new PinoTransport({ logger: pino() }),
});

Go has two constructors. The New/Build pair is the same pattern Go uses elsewhere when a misconfiguration is a programmer error you want to fail loudly on but still need a recoverable variant for env-driven setup:

loglayer.New(Config) *LogLayer is the typical entry point: it panics on misconfiguration (matches Go convention for setup-time errors). For applications that prefer explicit error handling on missing or invalid config, use loglayer.Build(Config) (*LogLayer, error), which returns loglayer.ErrNoTransport, ErrTransportAndTransports, or ErrUngroupedTransportsWithoutMode instead of panicking.

go
// Panics on misconfiguration (typical setup).
log := loglayer.New(loglayer.Config{
    Transport: structured.New(structured.Config{}),
})

// Or explicit error handling.
log, err := loglayer.Build(loglayer.Config{
    Transport: structured.New(structured.Config{}),
})

Errors ​

TypeScript errors carry a stack trace by default (the Error constructor in V8/JS engines). Go's error interface is just interface { Error() string }: no stack trace and no chain unless the error implementation provides one.

We recommend github.com/rotisserie/eris for stack-trace-bearing errors. Its ToJSON plugs straight into LogLayer's ErrorSerializer:

go
import "github.com/rotisserie/eris"

loglayer.New(loglayer.Config{
    Transport: structured.New(structured.Config{}),
    ErrorSerializer: func(err error) map[string]any {
        return eris.ToJSON(err, true) // include stack trace
    },
})

See Error Handling for the full reference.

Threading ​

This is where Go and TS genuinely diverge. JavaScript runs on a single-threaded event loop; TS code never has true parallelism within a process. Go has goroutines and a real shared-memory threading model.

LogLayer for Go's contract:

  • Every method on *LogLayer is safe to call from any goroutine, including concurrently with emission.
  • WithFields, WithoutFields, Child, WithPrefix return a new logger; the receiver is unchanged. (This matches the convention used by zerolog, zap, slog, and logrus.) Always assign the result: log = log.WithFields(...).
  • Level mutators, transport mutators, and mute toggles are all safe to call live (e.g. operator-driven debug toggling via SIGUSR1, hot-reload of transport lists), with no special coordination on your side.

See the full thread-safety contract.

Per-request loggers ​

The pattern is the same in both languages: derive a per-request logger and pass it down. The Go port ships first-class HTTP middleware in integrations/loghttp so this is one line at server setup:

go
http.ListenAndServe(":8080", loghttp.Middleware(log, loghttp.Config{})(mux))

Inside a handler:

go
func handler(w http.ResponseWriter, r *http.Request) {
    log := loghttp.FromRequest(r) // includes requestId, method, path
    log.Info("processing")
}

Module layout ​

TypeScript's @loglayer/transport-pino, @loglayer/plugin-redaction, etc. are separate npm packages. The Go port follows the same model: the core is one module, and any transport/plugin with a third-party dep is its own module so consumers only pay for what they import.

TypeScriptGo
loglayergo.loglayer.dev/v3 (core + stdlib renderers)
@loglayer/transport-zerologgo.loglayer.dev/transports/zerolog/v3
@loglayer/transport-datadoggo.loglayer.dev/transports/datadog/v3
@loglayer/integration-elysiago.loglayer.dev/integrations/loghttp/v3 (etc.)

Sub-modules that re-export core types ship on their own /v3 paths; check the Transports overview and the loghttp / sloghandler pages for each module's path.

go get each module you actually need; the dependency graph stays focused on whatever you imported.

Plugins ​

The TypeScript plugin system maps directly, but the Go API is interface-based instead of object-with-methods. loglayer.Plugin is a one-method interface (ID() string); each lifecycle hook is its own optional interface that you implement on the same type.

TypeScript hook (on LogLayerPlugin)Go interface
onContextCalledloglayer.FieldsHook
onMetadataCalledloglayer.MetadataHook
onBeforeDataOutloglayer.DataHook
onBeforeMessageOutloglayer.MessageHook
shouldSendToLoggerloglayer.SendGate
(no equivalent)loglayer.LevelHook

For one-off single-hook plugins, use the adapter constructors:

go
log.AddPlugin(loglayer.NewDataHook("tag-service", func(p loglayer.BeforeDataOutParams) loglayer.Data {
    return loglayer.Data{"service": "checkout"}
}))

The full set: NewFieldsHook, NewMetadataHook, NewDataHook, NewMessageHook, NewLevelHook, NewSendGate. For multi-hook plugins, declare a type implementing Plugin plus the relevant hook interfaces (the plugins/redact source is the canonical reference).

plugins/redact mirrors @loglayer/plugin-redaction. It supports key matching, regex value patterns, and json-tag-aware struct walking, all type-preserving:

go
import "go.loglayer.dev/plugins/redact/v3"

log.AddPlugin(redact.New(redact.Config{
    Keys:     []string{"password", "apiKey"},
    Patterns: []*regexp.Regexp{regexp.MustCompile(`^\d{16}$`)},
}))

See Plugins for the full lifecycle, hook ordering, and nil-return semantics. Third-party plugins can use utils/maputil for the same reflection-based deep-clone primitive that the redact plugin uses.

Groups ​

Groups port directly. The TS string | string[] argument shape becomes Go variadic, and null for "clear filter" becomes a separate ClearActiveGroups method.

TypeScriptGo
log.withGroup('database')log.WithGroup("database")
log.withGroup(['database', 'auth'])log.WithGroup("database", "auth")
log.addGroup(name, { ... })log.AddGroup(name, loglayer.LogGroup{...})
log.disableGroup(name) / enableGroup(name)log.DisableGroup(name) / EnableGroup(name)
log.setGroupLevel(name, 'debug')log.SetGroupLevel(name, loglayer.LogLevelDebug)
log.setActiveGroups(['db'])log.SetActiveGroups("db")
log.setActiveGroups(null)log.ClearActiveGroups()
log.getGroups()log.GetGroups() (shallow copy)
LOGLAYER_GROUPS env var auto-readConfig.Routing.ActiveGroups: loglayer.ActiveGroupsFromEnv("LOGLAYER_GROUPS") (explicit)
ungroupedBehavior: 'all' | 'none' | string[]Config.Routing.Ungrouped: loglayer.UngroupedRouting{Mode, Transports} (typed enum)

The Go port does not auto-read environment variables (libraries shouldn't); ActiveGroupsFromEnv is a helper you opt into. See Groups for the full reference.

Currently out of scope ​

These exist in TypeScript loglayer but are not yet implemented in the Go port:

  • Mixins: the useLogLayerMixin augmentation pattern.
  • Context managers: LinkedContextManager, IsolatedContextManager. Go's flat fields-as-map model covers most use cases.
  • Lazy evaluation: withMetadataLazy, withContextLazy. Possible to add but the ergonomic story is weaker in Go.

If any of these are blockers for your use case, open an issue at github.com/loglayer/loglayer-go.

Quick reference ​

go
// Equivalent of:
//   const log = new LogLayer({ transport: new PinoTransport({ logger: pino() }) });
//   log.withContext({ reqId: 'abc' });
//   log.withMetadata({ duration: 42 }).withError(err).info('did the thing');

import (
    "go.loglayer.dev/v3"
    "go.loglayer.dev/transports/structured/v3"
)

log := loglayer.New(loglayer.Config{
    Transport: structured.New(structured.Config{}),
})
log = log.WithFields(loglayer.Fields{"reqId": "abc"})
log.WithMetadata(loglayer.Metadata{"duration": 42}).
    WithError(err).
    Info("did the thing")

Same JSON output shape; same mental model.