loginjector

package module
v1.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 19 Imported by: 0

README

LogInjector

A small, dependency-light Go logging library. One Logger fans each message out to several sinks at once — size-rotated files, the console, a Telegram chat — filtered by severity levels you define. Application code logs through the standard library (log.New over an io.Writer); LogInjector supplies that writer and handles routing, rotation, and alerts.

Features

  • Fan-out — one Logger writes to many io.Writer sinks at once; safe for concurrent use.
  • Your own levels — no baked-in DEBUG/INFO; higher int = more severe. The optional levels sub-package ships a ready Debug..Critical ladder.
  • Ready handlers — rotating files, timestamped console, Telegram, with an optional per-handler minimum level.
  • Level hooks — tee alerts (e.g. to Telegram) on exact levels without lowering the global threshold.
  • Stdlib-native usage — hand components a *log.Logger via StdLog; they never import loginjector.
  • Opt-in HTTP tapping (httptap) — payload dump, access log, and panic-recover middleware, with always-on redaction of auth/cookie headers.
  • Dependency-light — no third-party runtime dependencies.

Install

go get github.com/prorochestvo/loginjector

Requires Go 1.22+.

Quick start

import (
	"github.com/prorochestvo/loginjector"
	"github.com/prorochestvo/loginjector/levels"
)

// size-rotated files under ./logs (5 MiB × 7) + timestamped console echo.
l, err := loginjector.NewFileLogger("./logs", "app", levels.Info)
if err != nil {
	panic(err)
}
l.Printf(levels.Info, "user %q signed in", "alice")

Recommended: build one Logger in main, then give each component its own standard-library front logger with StdLog(level, prefix) — the component depends on *log.Logger, not on loginjector:

// everything at Error and above is also delivered to Telegram.
l.Hook(loginjector.TelegramHandler(botToken, chatID, "errors.log", "prod"),
	levels.Error, levels.Severe, levels.Critical)

dbLog := l.StdLog(levels.Info, "sqlite ")  // = log.New(l.WriterAs(Info), "sqlite ", log.Lmsgprefix)
aiErr := l.StdLog(levels.Error, "openai ")
svc := NewService(dbLog, aiErr)            // takes *log.Logger

// inside the component, call sites stay standard library:
s.log.Printf("request sent (model=%s)", model) // "sqlite request sent (model=gpt-4o)"

The full API — handlers, error / JSON / closer helpers, httptap middleware, and every option — is on pkg.go.dev.

Notes

  • Panicf/Panic panic, they do not call os.Exit — a deferred recover can intercept them.
  • Emitter flags: StdLog sets log.Lmsgprefix (prefix only) because the sink stamps the timestamp; a hand-built front logger with log.LstdFlags doubles it.
  • httptap redaction is always on: Authorization, Proxy-Authorization, Cookie, and Set-Cookie are never logged. NewRecoverHandler buffers the response body, so keep it off SSE / WebSocket / streaming routes.
  • Stable-path rotation: RotatingFileHandler(dir, prefix, WithStableCurrentName()) keeps the live file at a fixed prefix.log path (backups stay indexed) so external tooling can follow it with tail -F; pair it with WithMaxAge and WithCompress for age-based retention and gzipped backups.

License

MIT — see LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CloseOrJoin added in v1.0.6

func CloseOrJoin(errp *error, c io.Closer)

CloseOrJoin closes c and joins any close error into *errp using errors.Join, which drops nil arguments and returns nil when all arguments are nil. The intended usage is

defer CloseOrJoin(&err, c)

inside a function with a named (... err error) return — the deferred call runs after the function body and joins the close error into whatever err holds at that point. A nil errp means the close error has nowhere to be stored; c is still closed and the error is silently discarded. A nil c is a no-op.

func CloseOrLog

func CloseOrLog(closer io.Closer)

CloseOrLog closes the closer and logs the error if there is one.

func CloseOrLogError added in v1.0.6

func CloseOrLogError(w io.Writer, c io.Closer)

CloseOrLogError closes c and, if Close returns a non-nil error and w is non-nil, writes the error message followed by a newline to w. It differs from CloseOrLog in that it writes to the caller-supplied writer rather than the standard library log package, making it suitable for structured logging (e.g. logger.WriterAs(level)). A nil w means the write is skipped but c is still closed. A nil c is a no-op. The write error is intentionally discarded — writing to a logger is the documented fmt.Fprint*-to-logger exception to the no-skipped-errors rule.

func CloseOrPanic

func CloseOrPanic(closer io.Closer)

CloseOrPanic closes the closer and panics if there is an error.

func FileByFormatHandler

func FileByFormatHandler(folder string, maxFilesInFolder int, fileNameGenerator func() string) io.Writer

FileByFormatHandler save messages to files by format. The file name is generated by fileNameGenerator. The folder is the directory where the files are saved. The maxFilesInFolder is the maximum number of files in the folder. No fresh-start variant exists for this handler; see RotatingFileHandler's WithFreshStart for an analogous feature on the index-based handler (there the construction-time target is known, whereas here the name is generated per-write by fileNameGenerator).

func PrintHandler

func PrintHandler() io.Writer

PrintHandler writes each message to os.Stdout as a plain line, trimming surrounding whitespace and appending a single newline. It adds no timestamp; for the timestamped console sink (which is the default when NewLogger is given no handlers) use TimestampedPrintHandler. The returned writer is mutex-guarded; the logger never calls its Write concurrently.

func RotatingFileHandler added in v1.0.6

func RotatingFileHandler(folder, prefix string, opts ...RotatingFileOption) io.Writer

RotatingFileHandler saves messages to files by number. The file name is generated from prefix and an incrementing index (e.g. prefix.00000001.log). When a file exceeds the maximum size the handler moves to the next index and prunes the oldest files so that no more than the configured maximum remain in folder. The handler appends to files and ring-prunes the oldest ones; it never overwrites a file's existing contents unless WithFreshStart is passed.

On construction the handler resumes at the highest existing index for prefix in folder (index 1 when none exist or WithFreshStart is set) and seeds the in-memory size counter from that file's on-disk size, so the first write after a restart accounts for existing content instead of writing into the oldest file and letting pruning delete the newest data. Filenames in folder that do not match the exact prefix.<8 hex>.log shape are ignored. Any stat/glob error encountered while resolving the resume point is stashed and joined into the first Write's return value.

Defaults: max file size 5 MiB, max files 7, no age bound, no compression, index-named live file. Override with WithMaxFileSize and WithMaxFiles; use WithFreshStart to begin each run from a truncated index-1 file. The opt-in options WithStableCurrentName (fixed prefix.log live path for tail -F), WithMaxAge (mtime-based retention), and WithCompress (gzip rotated backups to prefix.<8 hex>.log.gz) layer on additively; with none of them the on-disk output is byte-identical to a plain rotating handler. FileByFormatHandler is the sibling for filename-generator-driven rotation and has no fresh-start variant (its target name is not known until the first write).

The returned writer is mutex-guarded; the logger never calls its Write concurrently, and compression (when enabled) runs synchronously inside that lock. A single process must own a given (folder, prefix) pair; concurrent writers from multiple processes are unsupported under WithStableCurrentName and WithCompress.

func RuntimeDetailsFrozen added in v1.0.9

func RuntimeDetailsFrozen() bool

RuntimeDetailsFrozen reports whether the process-wide runtime-details string has already been computed and cached (frozen). Once it returns true, a subsequent SetRuntimeDetailsProvider call is a no-op — the provider will never run and its detail will not appear in any StackTraceError's Runtime().

This is an advisory diagnostic, not a synchronization primitive: it reports the freeze state at the moment of the call. It gives no guarantee that a provider set immediately after a false result will take effect (a concurrent StackTraceError construction may freeze the string in between). Use it at startup to warn that a provider was registered too late, not to gate registration.

func SetRuntimeDetailsProvider added in v1.0.6

func SetRuntimeDetailsProvider(fn func() string)

SetRuntimeDetailsProvider installs a process-wide provider that supplies additional runtime detail text appended to every StackTraceError's Runtime() output. The provider is called at most once per process; its return value is cached for the lifetime of the process, mirroring the caching behaviour of the built-in os/arch/Go-version runtime string.

Call this at startup, before any StackTraceError is constructed. Calling it after a StackTraceError has been constructed has NO effect, because the runtime string is cached on first use (see Runtime() docs). Calling it more than once: the last call before the cache is populated wins; calls after the cache is populated are no-ops.

The provider must be safe to call from any goroutine. Loginjector calls it under the same sync.Once that guards the built-in runtime string, so the provider sees no concurrent invocation; once cached, Runtime() never calls it again.

Provider must not panic; a panic is recovered and the built-in base string (os/arch/go version) is used without the extra detail.

Only one provider is active at a time; a subsequent SetRuntimeDetailsProvider call before the cache is populated replaces the first with no composition.

The provider's return value is sanitized to keep Runtime() single-line: CR, LF, and TAB are collapsed to spaces and other control characters are stripped, so a multi-line or control-char-laden provider result cannot break the single-line log contract. Printable Unicode is preserved.

Pass nil to clear a previously-set provider (useful in tests).

func StackRedacted added in v1.0.9

func StackRedacted(e StackTraceError) string

StackRedacted returns e.Stack() with each frame's absolute source path shortened to "<parent>/<base>" — the identical reduction LineTrace applies (e.g. "/home/ci/proj/main.go:10" becomes "proj/main.go:10"). The line numbers, goroutine header, and frame ordering are preserved; only the leading directory prefix of each frame path is dropped. This removes build-host absolute path prefixes, making the output safe to surface where Stack() is not.

It is a package-level function rather than a method on StackTraceError so the interface stays sealed: adding a method to the exported interface would break any external type that implements it.

func TelegramHandler

func TelegramHandler(botToken, chatID, fileName string, labels ...string) io.Writer

func TimestampedHandler added in v1.0.6

func TimestampedHandler(inner io.Writer, opts ...PrintOption) io.Writer

TimestampedHandler wraps inner so each message is prefixed with a leading timestamp before being forwarded to inner, indenting the continuation lines of a multi-line message so the body reads as a block. It is the composable form of TimestampedPrintHandler: any io.Writer (a file handler, a buffer, another sink) can be given sink-side timestamps. The returned writer is mutex-guarded; the logger never calls its Write concurrently.

The timestamp layout defaults to "2006/01/02 15:04:05". The first line of a message is prefixed with "<timestamp> "; subsequent lines are indented by the same width so multi-line payloads read as a visual block. Trailing whitespace and newlines are trimmed from the input before formatting; the output always ends with exactly one newline. An empty or whitespace-only input produces a single "<timestamp>\n" line.

WithTimeLayout and withClock apply; WithOutput is ignored because the sink is the explicit inner argument. Use TimestampedPrintHandler when you want the os.Stdout instantiation with WithOutput support.

func TimestampedPrintHandler added in v1.0.6

func TimestampedPrintHandler(opts ...PrintOption) io.Writer

TimestampedPrintHandler prints each message to stdout with a leading timestamp, indenting the continuation lines of a multi-line message so the body reads as a block. Unlike PrintHandler, which writes to stdout via a plain writer, this handler adds a timestamp; the sink is os.Stdout by default and overridable with WithOutput. The returned writer is mutex-guarded; the logger never calls its Write concurrently. It is the os.Stdout instantiation of TimestampedHandler.

The timestamp layout defaults to "2006/01/02 15:04:05". The first line of a message is prefixed with "<timestamp> "; subsequent lines are indented by the same width so multi-line payloads read as a visual block. Trailing whitespace and newlines are trimmed from the input before formatting; the output always ends with exactly one newline. An empty or whitespace-only input produces a single "<timestamp>\n" line.

func WithMinLevel added in v1.0.6

func WithMinLevel(level LogLevel, handler io.Writer) io.Writer

WithMinLevel wraps handler so that Logger treats it as a LeveledHandler with the given minimum level. The returned writer forwards Write calls to handler unchanged; the only added behaviour is the MinLevel() method that Logger.WriteLog reads via interface assertion. Use this to give one handler a threshold distinct from the parent Logger's minimumLogLevel — for example, to send only high-severity messages to the console while sending everything to a file:

file    := loginjector.RotatingFileHandler("./logs", "app")
console := loginjector.WithMinLevel(levels.Warning, loginjector.TimestampedPrintHandler())
logger  := loginjector.NewLogger(levels.Info, file, console)

The returned writer is itself thread-safe via Logger's existing ensureThreadSafe wrapping at registration. Calling WithMinLevel on an already-leveled writer replaces the inner MinLevel().

Types

type FileLoggerOption added in v1.0.6

type FileLoggerOption func(*fileLoggerConfig)

FileLoggerOption configures NewFileLogger.

func WithFileCompression added in v1.0.8

func WithFileCompression() FileLoggerOption

WithFileCompression forwards RotatingFileHandler's WithCompress to the underlying rotating handler: each rotated file is gzipped to prefix.<8 hex>.log.gz and the plaintext removed, synchronously on the triggering write. It is OFF by default.

RotatingFileHandler's WithStableCurrentName is deliberately NOT exposed here: NewFileLogger wraps the handler in TimestampedHandler, prefixing every line with a timestamp, which is at odds with a stable tail-able access-log format. Consumers wanting the fixed prefix.log live path build a RotatingFileHandler directly instead.

func WithMaxFileAge added in v1.0.8

func WithMaxFileAge(d time.Duration) FileLoggerOption

WithMaxFileAge forwards RotatingFileHandler's WithMaxAge to the underlying rotating handler: rotated files whose mtime is older than d are pruned on rotation, on top of the WithMaxFilesInFolder count bound. A d of zero or negative disables age pruning, which is the default. The unit is a time.Duration, so pass the full span — WithMaxFileAge(14 * 24 * time.Hour) for two weeks, not WithMaxFileAge(14) (14 ns).

func WithMaxFileCapacity added in v1.0.6

func WithMaxFileCapacity(n uint32) FileLoggerOption

WithMaxFileCapacity overrides the maximum size of a single log file in bytes before the handler rotates to the next file. The default is 5 MiB (5 * 1024 * 1024). The underlying RotatingFileHandler takes a uint32, so values larger than math.MaxUint32 are not valid; pass a reasonable capacity.

func WithMaxFilesInFolder added in v1.0.6

func WithMaxFilesInFolder(n int) FileLoggerOption

WithMaxFilesInFolder overrides the maximum number of rotated log files retained in the log folder. Older files beyond the limit are pruned on rotation. The default is 7.

func WithPrinterMinLevel added in v1.0.6

func WithPrinterMinLevel(level LogLevel) FileLoggerOption

WithPrinterMinLevel overrides the threshold at which the console printer emits messages, decoupling it from the file-handler threshold (minLevel). When not set, the printer fires at minLevel (current behaviour). When set, the console printer fires at level >= this value while the file handler continues to fire at level >= minLevel.

Has no effect when combined with WithoutPrinter — WithoutPrinter wins.

Implemented by wrapping the printer handler in WithMinLevel before adding it to the Logger handler list; the per-handler gate in Logger.WriteLog does the rest.

func WithPrinterOptions added in v1.0.6

func WithPrinterOptions(opts ...PrintOption) FileLoggerOption

WithPrinterOptions forwards PrintOption values to the TimestampedPrintHandler created by NewFileLogger. Has no effect when combined with WithoutPrinter.

func WithStdLogRedirect added in v1.0.6

func WithStdLogRedirect(level LogLevel) FileLoggerOption

WithStdLogRedirect redirects the standard library log package output to the new logger at the given level and sets log.Flags to 0. This option is OFF by default because log.SetOutput mutates global process state shared by every package in the binary — opting in is a conscious, explicit decision. If WithStdLogRedirect is passed more than once, the last call wins. If two NewFileLogger calls in the same process both use this option, the second call silently wins: log.SetOutput is global and the first logger will no longer receive standard library log output.

func WithTempDirFallback added in v1.0.6

func WithTempDirFallback() FileLoggerOption

WithTempDirFallback enables the temp-directory fallback: when the folder argument to NewFileLogger is empty, logs are written under filepath.Join(os.TempDir(), "logs") instead of returning an error. This option is OFF by default — an empty folder without this option is a hard error, so logs never silently land in an unexpected location. Note that the shared temp path is a residual risk: on multi-user systems another process could create the directory before NewFileLogger and own it with permissive permissions; NewFileLogger calls os.Chmod after MkdirAll to enforce 0750, but there is a brief window between directory creation and the chmod call.

func WithoutPrinter added in v1.0.6

func WithoutPrinter() FileLoggerOption

WithoutPrinter disables the timestamped stdout printer that NewFileLogger attaches by default. Use this when you want only the rotated file output and no console echo.

type HookID

type HookID string

HookID is a unique identifier for a hook

type HttpError

type HttpError interface {
	StatusCode() int
	error
}

HttpError is an error that provides an HTTP status code.

func NewHttpError

func NewHttpError(code int) HttpError

NewHttpError creates a new HttpError with the given status code.

func WrapHttpError added in v1.0.6

func WrapHttpError(code int, cause error) HttpError

WrapHttpError creates an HttpError that carries an underlying cause. StatusCode() returns code, Error() returns the HTTP status text followed by the cause detail, and errors.Unwrap traverses to cause. For standard HTTP codes with a nil cause, Error() matches NewHttpError(code). For unrecognised codes where http.StatusText returns an empty string, Error() falls back to "HTTP <code>" so the string is never empty — this diverges from NewHttpError(code), which returns "" for such codes; switching from NewHttpError to WrapHttpError(code, nil) on a bogus code therefore changes the Error() output.

type LeveledHandler added in v1.0.6

type LeveledHandler interface {
	io.Writer
	MinLevel() LogLevel
}

LeveledHandler is an optional interface a handler may implement to declare a minimum log level distinct from the parent Logger's minimumLogLevel. When a handler implements LeveledHandler, Logger.WriteLog uses its MinLevel() instead of Logger.minimumLogLevel for the per-handler gate. Handlers that do not implement LeveledHandler keep the historical behaviour — they fire when level >= Logger.minimumLogLevel.

MinLevel must be safe to call concurrently and must return the same value for the lifetime of the handler — Logger caches no result.

type LogLevel

type LogLevel int

LogLevel is a log level

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger is describing the logger structure and its methods

func NewFileLogger added in v1.0.6

func NewFileLogger(folder, prefix string, minLevel LogLevel, opts ...FileLoggerOption) (*Logger, error)

NewFileLogger builds a Logger that writes to size-rotated files under folder and, by default, mirrors every message at or above minLevel to a timestamped stdout printer (TimestampedPrintHandler). The printer fires as a handler (level >= minLevel), not a hook, so its gating matches the file handler exactly.

File lines are timestamped: the rotated file handler is wrapped in TimestampedHandler, so each on-disk line is "<timestamp> <message>" (layout "2006/01/02 15:04:05"), not the bare message. The console printer and the file handler are two separate sinks, each stamped exactly once.

The standard library log package is NOT redirected unless WithStdLogRedirect is passed — that redirect mutates global process state. An empty folder is rejected unless WithTempDirFallback is passed, in which case logs land under filepath.Join(os.TempDir(), "logs"). An empty prefix is always rejected because a hidden dotfile log (prefix ".") is a footgun.

Defaults: maxFileCapacity = 5 MiB, maxFilesInFolder = 7, printer ON, std-log redirect OFF, temp fallback OFF.

func NewLogger

func NewLogger(min LogLevel, handlers ...io.Writer) *Logger

NewLogger creates a new logger with the given minimum log level and handlers. If no handlers are provided, a default TimestampedPrintHandler (timestamped stdout) is added. Each handler is wrapped so the logger never calls its Write concurrently, which makes logging safe from multiple goroutines even with non-thread-safe handlers.

func (*Logger) Hook

func (l *Logger) Hook(writer io.Writer, level LogLevel, additional ...LogLevel) HookID

Hook registers writer to fire on an EXACT match of any of the given levels (level plus additional), regardless of the logger's minimum level. Duplicate levels are collapsed to a single registration, so Hook(w, X, X) writes to w once per matching WriteLog, not twice. The returned HookID removes every registration created by this call when passed to Unhook.

Hooks match on exact equality, not a threshold: a hook on level X does NOT fire for a message logged at level X+1. To alert on "level >= X" — the common case for routing high-severity messages to an out-of-band sink — register a WithMinLevel(X, writer) handler instead of enumerating exact levels, because an enumeration that omits one level silently drops that level's messages.

The returned HookID is unique only within this logger; do not persist or compare it against a fixed format.

func (*Logger) Panic added in v1.0.6

func (l *Logger) Panic(level LogLevel, args ...any)

Panic writes a log message at the given level and then panics with the message string. The panic is recoverable: deferred functions run and callers may use recover() to catch it. It does NOT call os.Exit — process death is not guaranteed.

func (*Logger) Panicf added in v1.0.6

func (l *Logger) Panicf(level LogLevel, format string, args ...any)

Panicf writes a formatted log message at the given level and then panics with the message string. The panic is recoverable: deferred functions run and callers may use recover() to catch it. It does NOT call os.Exit — process death is not guaranteed.

func (*Logger) Print

func (l *Logger) Print(level LogLevel, args ...any)

Print writes a log message

func (*Logger) Printf

func (l *Logger) Printf(level LogLevel, format string, args ...any)

Printf writes a formatted log message

func (*Logger) SetMinLevel

func (l *Logger) SetMinLevel(level LogLevel)

SetMinLevel sets the minimum log level

func (*Logger) StdLog added in v1.0.6

func (l *Logger) StdLog(level LogLevel, prefix string) *log.Logger

StdLog returns a standard-library *log.Logger that writes through this logger at the given level, with the given prefix. It is configured with log.Lmsgprefix and no date/time flags: the emitter contributes only the prefix, and any timestamp is added by the sink (e.g. TimestampedPrintHandler or TimestampedHandler), so a line is never stamped twice. Use it to hand a *log.Logger to code that expects one — for example a database driver's logger — without hand-wiring the flags:

dbLog := logger.StdLog(levels.Info, "sqlite ")

Each call returns a fresh *log.Logger over a fresh writer; the returned logger routes through WriteLog, which serializes writes per sink.

func (*Logger) Unhook

func (l *Logger) Unhook(id HookID)

Unhook removes a hook from the logger

func (*Logger) Write

func (l *Logger) Write(message []byte) (int, error)

Write writes a log message with the minimum log level

func (*Logger) WriteLog

func (l *Logger) WriteLog(level LogLevel, message []byte) (int, error)

WriteLog writes a log message at the given level to all matching sinks.

Hooks fire on an exact level match regardless of minimumLogLevel. Handlers fire only when level >= minimumLogLevel. When no sinks match (below the minimum and no matching hooks), WriteLog returns (0, nil). When exactly one sink matches it is written inline (no goroutine overhead). Two or more sinks run concurrently, each in its own goroutine, with errors joined via errors.Join.

WriteLog holds the read lock for the entire duration so it is safe to call concurrently with Hook/Unhook/SetMinLevel.

func (*Logger) WriterAs

func (l *Logger) WriterAs(level LogLevel) io.Writer

WriterAs returns a writer that writes to the logger as the given log level

type PrintOption added in v1.0.6

type PrintOption func(*printConfig)

PrintOption configures TimestampedPrintHandler.

func WithOutput added in v1.0.6

func WithOutput(w io.Writer) PrintOption

WithOutput overrides the sink used by TimestampedPrintHandler. The default is os.Stdout. This is primarily intended to redirect output for testing, but it is a legitimate consumer need (e.g. writing timestamped messages to a file instead of stdout).

func WithTimeLayout added in v1.0.6

func WithTimeLayout(layout string) PrintOption

WithTimeLayout overrides the timestamp layout used by TimestampedPrintHandler. The default layout is "2006/01/02 15:04:05".

type PublicDetailsError added in v1.0.6

type PublicDetailsError interface {
	PublicError
	// Details returns the joined client-safe text. It is an alias of
	// PublicMessage and always returns the same value.
	Details() string
}

PublicDetailsError extends PublicError with a Details accessor that returns the same client-safe text as PublicMessage. It is the return type of NewPublicErrorDetails and exists so callers using the variadic constructor have an accessor name that matches the common consumer convention.

func NewPublicErrorDetails added in v1.0.6

func NewPublicErrorDetails(parts ...string) PublicDetailsError

NewPublicErrorDetails creates a PublicDetailsError by joining the given parts with a single space. The result has no internal cause; Error() and PublicMessage() both return the joined string. It is a convenience for the common pattern of building a public message from several parts and is semantically identical to NewPublicError(strings.Join(parts, " "), nil).

type PublicError added in v1.0.6

type PublicError interface {
	// PublicMessage returns the client-safe text. It never includes any detail
	// from the internal cause, even if the public string is empty.
	PublicMessage() string
	error
}

PublicError separates a user-safe public message from the internal cause. Error() returns the internal detail for logging; PublicMessage() returns only the text that is safe to expose to clients and never leaks the internal cause.

func NewPublicError added in v1.0.6

func NewPublicError(public string, cause error) PublicError

NewPublicError creates a PublicError from a client-safe message and an internal cause. Error() returns the cause's error text for logging, falling back to the public message only when cause is nil. PublicMessage() always returns only the public string. Unwrap returns cause, so errors.Is and errors.As traverse to it.

type RotatingFileOption added in v1.0.6

type RotatingFileOption func(*rotatingFileConfig)

RotatingFileOption configures RotatingFileHandler.

func WithCompress added in v1.0.8

func WithCompress() RotatingFileOption

WithCompress gzips each rotated backup to prefix.<8 hex>.log.gz and removes the plaintext, synchronously, on the Write that triggers the rotation. It is OFF by default; with no options the handler never compresses and a pre-existing foreign *.log.gz in the folder is ignored.

Compression is crash-safe: the gzip is written to a temp file, fsync'd, and renamed into place before the plaintext is removed, so a reader of *.log.gz never sees a partial file. If a crash leaves both prefix.<idx>.log and prefix.<idx>.log.gz for one index, the .gz is authoritative and the plaintext is discarded on the next construction. A .log/.log.gz pair counts as a single file for the WithMaxFiles bound.

Because it runs under the handler mutex, compression adds a bounded latency spike to the one Write per rotation; the async background variant is deferred. The handler never appends into a .gz file.

func WithFileMode added in v1.0.9

func WithFileMode(mode os.FileMode) RotatingFileOption

WithFileMode overrides the permission bits used when the handler CREATES a log file — the live prefix.<8 hex>.log (or prefix.log under WithStableCurrentName) and the gzip .log.gz temp under WithCompress. The default is 0640.

It applies only to files this handler creates; the process umask still masks the bits (the effective mode is only ever more restrictive than requested). It does NOT re-chmod a file that already exists on disk — a live file left at 0640 by a prior run keeps 0640 because it is opened O_APPEND, not created. Pass WithFreshStart if you need the mode to apply to a from-scratch ring.

func WithFreshStart added in v1.0.6

func WithFreshStart() RotatingFileOption

WithFreshStart makes the handler remove every existing prefix.<8 hex>.log file in folder once at construction, before the first write, so each process run begins with a genuinely empty ring at index 1. Existing rotated content is permanently destroyed — this is the intended behaviour; omit the option to resume and preserve prior content across restarts. Files in folder that do not match the prefix.<8 hex>.log shape are left untouched. When combined with WithCompress the compressed prefix.<8 hex>.log.gz backups and any interrupted prefix.*.log.gz.tmp temps are removed too; with WithStableCurrentName the fixed prefix.log live file is removed as well — so the ring is genuinely empty under every option combination.

A missing file or folder is a no-op: no file is created at construction, and the first write creates index-1. Any removal error is stashed and surfaced on the first Write's return value, then cleared. WithFreshStart forces the starting index to 1 and the seeded size to 0, overriding the resume-at-highest-index behaviour.

func WithMaxAge added in v1.0.8

func WithMaxAge(d time.Duration) RotatingFileOption

WithMaxAge prunes rotated backups whose file modification time is older than d on each rotation, on top of the WithMaxFiles count bound (a backup is removed when either bound is exceeded). The live file is never pruned. A d of zero or negative disables age pruning; that is the default.

The unit is a time.Duration, so pass the full span — WithMaxAge(14 * 24 * time.Hour) for two weeks. WithMaxAge(14) is fourteen nanoseconds, i.e. an instant wipe of every backup on the first rotation.

Age is measured by mtime, which is unreliable across NFS clock skew, a cp without -p, or a restore that resets timestamps. When WithCompress is also set the compressed backup keeps the source file's mtime (via os.Chtimes), so a genuinely old log compressed today still ages out.

func WithMaxAgeDays added in v1.0.9

func WithMaxAgeDays(days int) RotatingFileOption

WithMaxAgeDays is WithMaxAge(time.Duration(days) * 24 * time.Hour) with an unmistakable unit — it prunes rotated backups older than the given number of days. See WithMaxAge for the full pruning semantics, mtime caveats, and the zero-disables rule; this is purely a units-safe wrapper over it.

func WithMaxFileSize added in v1.0.6

func WithMaxFileSize(n uint32) RotatingFileOption

WithMaxFileSize overrides the maximum size of a single log file in bytes before the handler rotates to the next index. The default is 5 MiB (5 << 20).

func WithMaxFiles added in v1.0.6

func WithMaxFiles(n int) RotatingFileOption

WithMaxFiles overrides the maximum number of rotated log files retained in the folder. Older files beyond the limit are pruned on rotation. The default is 7.

func WithStableCurrentName added in v1.0.8

func WithStableCurrentName() RotatingFileOption

WithStableCurrentName keeps the live file at a fixed path, prefix.log, instead of naming it after the current rotation index. On rotation the live file is renamed to the next indexed backup (prefix.<8 hex>.log) and a fresh prefix.log is opened, so external tooling can follow the log at a stable path with tail -F (not tail -f: the inode changes on every rotation). Rotated backups stay indexed exactly as in the default scheme.

On resume the handler seeds its size counter from prefix.log and appends to it; the next backup index is one past the highest existing prefix.<8 hex>.log[.gz]. Legacy indexed files already in the folder are adopted as backups and never chosen as the live target. Switching a folder between the indexed and stable schemes between runs is unsupported without WithFreshStart or a clean folder; a stale prefix.log left by a prior stable run must be drained manually when switching back to the indexed scheme.

Single-writer invariant: exactly one process may own a (folder, prefix) pair. Two processes renaming prefix.log concurrently race and lose backups; this is not defended in code (lumberjack has the same limitation).

type StackTraceError added in v1.0.6

type StackTraceError interface {
	// Stack returns the full debug.Stack() output captured at construction time.
	// The output contains absolute build-host file paths — do not forward it to
	// untrusted clients or HTTP responses.
	Stack() string
	// Runtime returns a string describing the OS, architecture, and Go version,
	// computed once on first use and cached for the process lifetime — every
	// StackTraceError value shares the same string. If
	// SetRuntimeDetailsProvider was called with a non-nil function before any
	// StackTraceError was constructed, the provider's output is appended to the
	// built-in os/arch/go descriptor.
	Runtime() string
	error
}

StackTraceError captures a full stack trace and runtime environment details at the point of construction. Error() returns only the message and cause for compact log lines; the full stack is available on demand via Stack(). Runtime details (OS, arch, Go version) are cached once for the process lifetime.

func NewStackTraceError added in v1.0.6

func NewStackTraceError() StackTraceError

NewStackTraceError creates a StackTraceError with the full call-stack captured at this call site. Error() returns an empty string because no message is supplied — call Stack() for the full trace. The empty Error() means this value contributes no text when joined via errors.Join or wrapped via fmt.Errorf("%w"); use NewStackTraceErrorf when you need a non-empty error string in the chain. Stack() contains absolute build-host file paths; do not forward it to untrusted clients.

func NewStackTraceErrorf added in v1.0.6

func NewStackTraceErrorf(format string, args ...any) StackTraceError

NewStackTraceErrorf creates a StackTraceError with a formatted message and the full call-stack captured at this call site. Error() returns the message only; use Stack() for the full trace.

func WrapStackTraceError added in v1.0.6

func WrapStackTraceError(cause error, format string, args ...any) StackTraceError

WrapStackTraceError creates a StackTraceError with a formatted message, the full call-stack captured at this call site, and an explicit cause. errors.Unwrap, errors.Is, and errors.As traverse to cause. Passing a nil cause is safe. The message is rendered with fmt.Sprintf — do NOT use %w in the format string; use the cause argument for wrapping.

type TraceError

type TraceError interface {
	Line() string
	error
}

TraceError is an error that provides a stack trace.

func NewTraceError

func NewTraceError() TraceError

NewTraceError creates a TraceError whose Line() is the caller's file:line (method). It is a pure leaf — it carries a line and no cause, so it has no Unwrap and does not participate in errors.Is/As traversal. Use NewTraceErrorf or WrapTraceError when you need a cause-carrying, unwrappable trace error.

func NewTraceErrorSkip added in v1.0.9

func NewTraceErrorSkip(skip int) TraceError

NewTraceErrorSkip creates a TraceError whose Line() is resolved skip frames above the caller of NewTraceErrorSkip. skip=0 is the direct caller; skip=1 is that caller's caller. Use it when wrapping NewTraceError in a helper so the line still points at your caller rather than the wrapper.

func NewTraceErrorf added in v1.0.6

func NewTraceErrorf(format string, args ...any) TraceError

NewTraceErrorf creates a TraceError with a formatted message and the current call-site line. The message is built via fmt.Errorf, so a %w verb in format wraps the corresponding argument and errors.Is/errors.As traverse to it (through one intermediate fmt.Errorf node — Unwrap is always non-nil for the value produced here; it just terminates with nil when no %w was used). To have Unwrap return your cause directly with a single hop, use WrapTraceError. The stack line is captured at the point of this call, not inside any helper.

func WrapTraceError added in v1.0.6

func WrapTraceError(cause error, format string, args ...any) TraceError

WrapTraceError creates a TraceError with a formatted message, the current call-site line, and an explicit cause. errors.Unwrap, errors.Is, and errors.As traverse to cause. Passing a nil cause is safe; Unwrap returns nil. The message is rendered with fmt.Sprintf — do NOT use %w in the format string, it would produce a "%!w(...)" garbage token; rely on the cause argument for wrapping, or use NewTraceErrorf if you need %w in the format itself.

Directories

Path Synopsis
Package httptap is the opt-in HTTP-traffic tap for github.com/prorochestvo/loginjector.
Package httptap is the opt-in HTTP-traffic tap for github.com/prorochestvo/loginjector.
synctest
Package synctest provides a mutex-guarded bytes.Buffer for the loginjector test suites.
Package synctest provides a mutex-guarded bytes.Buffer for the loginjector test suites.
Package levels provides an optional preset of named severity constants for the loginjector.LogLevel type.
Package levels provides an optional preset of named severity constants for the loginjector.LogLevel type.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL