Documentation
¶
Overview ¶
Package seilog provides structured logging with per-logger level control, built on top of log/slog.
seilog adds two things standard slog does not offer out of the box: hierarchical logger naming and the ability to change log levels at runtime without restarting the process. Every logger created through NewLogger returns a plain *slog.Logger, so callers use the standard library API they already know — there is no wrapper type and no lock-in.
Quick Start ¶
var log = seilog.NewLogger("myapp", "db")
func main() {
log.Info("connected", "host", "localhost")
seilog.SetLevel("myapp/*", slog.LevelDebug) // turn on debug for direct children of myapp
seilog.SetLevel("myapp/**", slog.LevelDebug) // turn on debug for all children of myapp
}
Logger Naming ¶
Logger names form a hierarchy separated by "/". The recommended convention is to mirror your module or package structure so that names are globally unique, predictable, and easy to target with glob patterns:
seilog.NewLogger("myapp") // top-level
seilog.NewLogger("myapp", "db") // → "myapp/db"
seilog.NewLogger("myapp", "db", "pool") // → "myapp/db/pool"
Each segment must match the pattern [a-z0-9]+(-[a-z0-9]+)*. This is enforced at creation time via panic. The constraint exists for three reasons:
- Consistency — uniform naming across a large codebase prevents typos like "MyApp" vs "myapp" from silently creating separate loggers.
- Glob safety — because segments cannot contain glob meta-characters (*, ?, [), a bare name is always an exact match in SetLevel and never accidentally interpreted as a pattern.
- Log hygiene — disallowing whitespace, newlines, and special characters keeps log output parseable and prevents injection into structured formats.
Use the variadic form of NewLogger rather than embedding "/" directly in a segment name. The variadic form makes the hierarchy explicit and is validated per-segment.
Good: "myapp", "http-server", "myapp/db/pool" Bad: "MyApp", "my app", "", "myapp//db"
Setting and Querying Levels ¶
Levels can be changed at runtime per logger or by pattern, and queried for diagnostics:
seilog.SetLevel("myapp/db", slog.LevelDebug) // exact match
seilog.SetLevel("myapp/*", slog.LevelDebug) // direct children of myapp
seilog.SetLevel("myapp/*/*", slog.LevelWarn) // grandchildren of myapp only
seilog.SetLevel("myapp/**", slog.LevelDebug) // myapp and ALL descendants
lvl, ok := seilog.GetLevel("myapp/db") // query current level
Glob patterns follow path.Match semantics. Each "*" matches a single path segment and does not cross "/" boundaries:
"myapp/*" matches "myapp/db" but NOT "myapp/db/pool" "myapp/*/*" matches "myapp/db/pool" but NOT "myapp/db" "*/db" matches "myapp/db" but NOT "myapp/v2/db"
seilog extends standard glob matching with two special patterns:
- "/**" suffix — recursive prefix match. "myapp/**" matches "myapp" itself and every logger whose name starts with "myapp/" at any depth. This is the primary way to adjust an entire subtree at once.
- "*" alone — matches every registered logger regardless of depth.
To change the baseline level for loggers that have not yet been created, use SetDefaultLevel. To inspect all registered logger names (e.g. for an admin endpoint), use ListLoggers.
Output and Lifecycle ¶
Output format, destination, and source-location recording are configured once at process startup through environment variables. These settings are read during package init and cannot be changed afterward; the handler is captured by each logger at creation time.
SEI_LOG_LEVEL — Default level: debug, info, warn, error (default: info).
SEI_LOG_FORMAT — Output format: json or text (default: text).
SEI_LOG_OUTPUT — Destination: stdout, stderr, or an absolute file path
(default: stdout). File paths must not contain ".."
components. Files are opened with mode 0600 and
O_APPEND for atomic POSIX writes. The operator is
responsible for ensuring the path is trusted.
seilog does not perform log rotation — pair with an
external tool such as logrotate when writing to files.
SEI_LOG_ADD_SOURCE — Include source file and line in output (default: false).
When SEI_LOG_OUTPUT points to a file, call Close during graceful shutdown to flush and close the file descriptor. Close is safe to call multiple times and is a no-op for stdout and stderr. If Close is not called, the operating system will close the descriptor on process exit, but buffered data may be lost.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Close ¶
func Close() error
Close closes the log output opened via the SEI_LOG_OUTPUT environment variable. It is a no-op when output is stdout or stderr.
Call Close during graceful shutdown to ensure the file descriptor is flushed and released. It is safe to call multiple times; only the first call performs the close. After Close returns, further log writes to the file may fail silently.
Typical usage:
func main() {
defer seilog.Close()
// ...
}
If Close is never called, the operating system will close the descriptor when the process exits, but any data buffered by the OS may be lost. See the package-level documentation under "Output and Lifecycle" for details on how output is configured.
func GetLevel ¶
GetLevel returns the current log level of a registered logger.
If a logger with the given name exists, GetLevel returns its level and true. If no logger with that name has been created via NewLogger, it returns 0 and false.
The returned level reflects the most recent change made by SetLevel, SetDefaultLevel, or the initial default — whichever was applied last to this logger.
GetLevel is intended for admin endpoints, diagnostics, and tests:
if lvl, ok := seilog.GetLevel("myapp/db"); ok {
fmt.Printf("myapp/db is at %s\n", lvl)
}
GetLevel is safe for concurrent use.
func ListLoggers ¶
func ListLoggers() []string
ListLoggers returns the names of all loggers registered via NewLogger. The returned slice is in no particular order.
This is useful for building admin or diagnostics endpoints that display registered loggers alongside their current levels (see GetLevel), or for verifying that a glob pattern passed to SetLevel will match the intended loggers before applying it.
ListLoggers is safe for concurrent use.
func NewLogger ¶
NewLogger creates a named logger whose level can be changed at runtime.
The returned *slog.Logger is a standard library logger — callers use the normal slog API (Info, Debug, With, WithGroup, etc.) with no seilog-specific wrapper.
Sub-segments are joined with "/" to form a hierarchical name:
seilog.NewLogger("myapp") // "myapp"
seilog.NewLogger("myapp", "db") // "myapp/db"
seilog.NewLogger("myapp", "db", "pool") // "myapp/db/pool"
Each segment must be lowercase alphanumerics and hyphens only, matching the pattern [a-z0-9]+(-[a-z0-9]+)*. This is enforced at creation time via panic because an invalid name is always a programmer error and should be caught immediately during development, not silently masked at runtime. See the package-level documentation for the rationale behind this constraint.
Use the variadic subs parameter rather than embedding "/" directly in a segment. The variadic form ensures each segment is validated individually and keeps naming consistent across a codebase.
Calling NewLogger multiple times with the same resolved name returns distinct *slog.Logger instances that share the same underlying slog.LevelVar. This means changing the level via SetLevel, SetDefaultLevel, or GetLevel affects every logger instance created with that name.
Each logger carries a "logger" attribute set to its resolved name, so log output can be filtered or searched by logger identity.
NewLogger is safe for concurrent use. It is intended to be called at package init time and the result stored in a package-level variable:
var log = seilog.NewLogger("myapp", "db")
func SetDefaultLevel ¶
SetDefaultLevel changes the baseline level applied to loggers created by future calls to NewLogger.
If updateExisting is true, every logger currently in the registry is also set to the new level. This is equivalent to calling SetLevel("*", level) followed by changing the default, and is the simplest way to uniformly adjust verbosity across the entire process.
If updateExisting is false, existing loggers retain whatever level they were last set to (via SetLevel or a previous call to SetDefaultLevel) and only newly created loggers inherit the new default. This is useful when you want to tighten the default without disrupting loggers that have been individually tuned.
SetDefaultLevel is safe for concurrent use.
func SetLevel ¶
SetLevel changes the log level at runtime for one or more loggers that have already been created by NewLogger.
The name argument can be an exact logger name, a glob pattern, or a recursive prefix:
seilog.SetLevel("myapp/db", slog.LevelDebug) // exact match
seilog.SetLevel("myapp/*", slog.LevelDebug) // direct children only
seilog.SetLevel("myapp/*/*", slog.LevelWarn) // grandchildren only
seilog.SetLevel("myapp/**", slog.LevelDebug) // myapp and all descendants
Glob patterns follow path.Match semantics. Each "*" in a glob matches a single path segment and does not cross "/" boundaries.
The "/**" suffix is a seilog-specific extension that matches the prefix logger itself and every logger whose name starts with that prefix followed by "/". For example, "myapp/**" matches "myapp", "myapp/db", and "myapp/db/pool".
As another seilog-specific extension, passing "*" alone matches every registered logger regardless of depth — this bypasses path.Match and iterates the full registry.
SetLevel only affects loggers that already exist in the registry. To also change the baseline for loggers created in the future, use SetDefaultLevel.
Returns the number of loggers whose level was changed. A return value of 0 means no registered logger matched the name or pattern — this can help detect typos. Use ListLoggers to inspect registered names and GetLevel to verify the result.
If the pattern is syntactically invalid (per path.Match), SetLevel returns 0 without modifying any logger.
SetLevel is safe for concurrent use. Level changes take effect immediately for all goroutines logging through the affected loggers.
Types ¶
This section is empty.