Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03642ec746 | |||
| 5eaea6ada4 | |||
|
|
e4d86eb0fb | ||
| c656fed80b | |||
|
|
3423bac33f | ||
| 7c984b22dc | |||
|
|
318a948a33 | ||
| 6008ca5c3a | |||
|
|
426b164b75 |
9 changed files with 271 additions and 45 deletions
61
SECURITY_ATTACK_VECTOR_MAPPING.md
Normal file
61
SECURITY_ATTACK_VECTOR_MAPPING.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Security Attack Vector Mapping
|
||||||
|
|
||||||
|
- `CLAUDE.md` reviewed.
|
||||||
|
- `CODEX.md` was not present in this repository.
|
||||||
|
- Scope: exported API entry points that accept caller-controlled data, plus direct OS/environment reads. Public field-based inputs (`Logger.Style*`, `Err.Op`, `Err.Msg`, `Err.Err`, `Err.Code`) are called out in the relevant rows because they are consumed by methods rather than setter functions.
|
||||||
|
- Common sink: `(*Logger).log` in `log.go:169`-`242` writes the final log line with `fmt.Fprintf(output, "%s %s %s%s\n", ...)` at `log.go:242`.
|
||||||
|
- Existing controls in the common sink: string values in `keyvals` are `%q`-escaped at `log.go:234`-`236`, exact-match redaction is applied with `slices.Contains(redactKeys, keyStr)` at `log.go:227`-`231`, and `Err`-derived `op` / `stack` context is auto-added at `log.go:178`-`211`.
|
||||||
|
- Gaps in the common sink: `msg`, key names, `error` values, and other non-string values are formatted without sanitisation at `log.go:221`-`242`.
|
||||||
|
|
||||||
|
## Logger Configuration And Emission
|
||||||
|
|
||||||
|
| Function | File:Line | Input source | Flows into | Current validation | Potential attack vector |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `New(opts Options)` | `log.go:111` | Caller-controlled `Options.Level`, `Options.Output`, `Options.Rotation`, `Options.RedactKeys`; optional global `RotationWriterFactory` | Initial `Logger` state; `opts.Output` or `RotationWriterFactory(*opts.Rotation)` becomes `Logger.output`; `RedactKeys` copied into `Logger.redactKeys` and later checked during logging | Falls back to `os.Stderr` if output is `nil`; only uses rotation when `Rotation != nil`, `Filename != ""`, and `RotationWriterFactory != nil`; `RedactKeys` slice is cloned | Path-based write/exfiltration if untrusted `Rotation.Filename` reaches a permissive factory; arbitrary side effects or exfiltration through a caller-supplied writer; log suppression if an untrusted config sets `LevelQuiet`; redaction bypass if secret keys do not exactly match configured names |
|
||||||
|
| `(*Logger).SetLevel(level Level)` | `log.go:136` | Caller-controlled log level | `Logger.level`, then `shouldLog()` in all log methods | None | Audit bypass or loss of forensic detail if attacker-controlled config lowers the level or uses unexpected numeric values |
|
||||||
|
| `(*Logger).SetOutput(w io.Writer)` | `log.go:150` | Caller-controlled writer | `Logger.output`, then `fmt.Fprintf` in `log.go:242` | None | Log exfiltration, unexpected side effects in custom writers, or panic/DoS if a nil or broken writer is installed |
|
||||||
|
| `(*Logger).SetRedactKeys(keys ...string)` | `log.go:157` | Caller-controlled key names | `Logger.redactKeys`, then `slices.Contains(redactKeys, keyStr)` during formatting | Slice clone only | Secret leakage when caller omits aliases, case variants, or confusable keys; CPU overhead grows linearly with large redact lists |
|
||||||
|
| `(*Logger).Debug(msg string, keyvals ...any)` | `log.go:246` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleDebug` field | `StyleDebug("[DBG]")` -> `(*Logger).log` -> `fmt.Fprintf` | Level gate only; string `keyvals` values are quoted; exact-match redaction for configured keys; no nil check on `StyleDebug` | Log forging or parser confusion via unsanitised `msg`, key names, `error` strings, or non-string `fmt.Stringer` values; secret leakage in `msg` or non-redacted fields; nil style function can panic |
|
||||||
|
| `(*Logger).Info(msg string, keyvals ...any)` | `log.go:253` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleInfo` field | `StyleInfo("[INF]")` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `Debug` | Same attack surface as `Debug`: log injection, secret leakage outside exact-key redaction, and nil-style panic/DoS |
|
||||||
|
| `(*Logger).Warn(msg string, keyvals ...any)` | `log.go:260` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleWarn` field | `StyleWarn("[WRN]")` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `Debug` | Same attack surface as `Debug`: log injection, secret leakage outside exact-key redaction, and nil-style panic/DoS |
|
||||||
|
| `(*Logger).Error(msg string, keyvals ...any)` | `log.go:267` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleError` field | `StyleError("[ERR]")` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `Debug` | Same attack surface as `Debug`; higher impact because error logs are more likely to be consumed by SIEMs and incident tooling |
|
||||||
|
| `(*Logger).Security(msg string, keyvals ...any)` | `log.go:276` | Caller-controlled `msg`, `keyvals`, and mutable `Logger.StyleSecurity` field | `StyleSecurity("[SEC]")` -> `(*Logger).log` -> `fmt.Fprintf` | Uses `LevelError` threshold so security events still emit when normal info logging is disabled; otherwise same controls as `Debug` | Spoofed or injected security events if untrusted text reaches `msg` / `keyvals`; secret leakage in high-signal security logs; nil-style panic/DoS |
|
||||||
|
| `Username()` | `log.go:284` | OS account lookup from `user.Current()` and environment variables `USER` / `USERNAME` | Returned username string to caller | Uses `user.Current()` first; falls back to `USER`, then `USERNAME`; no normalisation or escaping | Username spoofing in untrusted/containerised environments where env vars are attacker-controlled; downstream log/UI injection if callers print the returned value without escaping |
|
||||||
|
| `SetDefault(l *Logger)` | `log.go:305` | Caller-controlled logger pointer and its mutable fields (`output`, `Style*`, redact config, level) | Global `defaultLogger`, then all package-level log functions plus `LogError`, `LogWarn`, and `Must` | None | Nil logger causes later panic/DoS; swapping the default logger can redirect logs, disable redaction, or install panicking style/output hooks globally |
|
||||||
|
| `SetLevel(level Level)` | `log.go:310` | Caller-controlled log level | `defaultLogger.SetLevel(level)` | None beyond instance setter behaviour | Same as `(*Logger).SetLevel`, but process-global |
|
||||||
|
| `SetRedactKeys(keys ...string)` | `log.go:315` | Caller-controlled redact keys | `defaultLogger.SetRedactKeys(keys...)` | None beyond instance setter behaviour | Same as `(*Logger).SetRedactKeys`, but process-global |
|
||||||
|
| `Debug(msg string, keyvals ...any)` | `log.go:320` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Debug(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Debug`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Debug`, plus global DoS if `defaultLogger` was set to nil |
|
||||||
|
| `Info(msg string, keyvals ...any)` | `log.go:325` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Info(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Info`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Info`, plus global DoS if `defaultLogger` was set to nil |
|
||||||
|
| `Warn(msg string, keyvals ...any)` | `log.go:330` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Warn(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Warn`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Warn`, plus global DoS if `defaultLogger` was set to nil |
|
||||||
|
| `Error(msg string, keyvals ...any)` | `log.go:335` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Error(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Error`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Error`, plus global DoS if `defaultLogger` was set to nil |
|
||||||
|
| `Security(msg string, keyvals ...any)` | `log.go:340` | Caller-controlled `msg` and `keyvals` routed through the global logger | `defaultLogger.Security(...)` -> `(*Logger).log` -> `fmt.Fprintf` | Same controls as `(*Logger).Security`; additionally depends on `defaultLogger` not being nil | Same as `(*Logger).Security`, plus global DoS if `defaultLogger` was set to nil |
|
||||||
|
|
||||||
|
## Error Construction And Emission
|
||||||
|
|
||||||
|
| Function | File:Line | Input source | Flows into | Current validation | Potential attack vector |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `(*Err).Error()` | `errors.go:25` | Public `Err.Op`, `Err.Msg`, `Err.Err`, and `Err.Code` fields, including direct struct literal construction by callers | Returned error string via `fmt.Sprintf`; if the error is later logged as a non-string value, the text reaches `(*Logger).log` -> `fmt.Fprintf` | None | Log injection or response spoofing via attacker-controlled op/message/code text; secret disclosure via underlying error text; machine-readable code spoofing |
|
||||||
|
| `E(op, msg string, err error)` | `errors.go:56` | Caller-controlled `op`, `msg`, `err` | New `Err` instance, then `(*Err).Error()` when rendered | None | Same as `(*Err).Error()`: attacker-controlled context and underlying error text can be surfaced in logs or responses |
|
||||||
|
| `Wrap(err error, op, msg string)` | `errors.go:67` | Caller-controlled `err`, `op`, `msg` | New `Err` wrapper; preserves existing `Code` from wrapped `*Err` if present | Returns `nil` when `err == nil`; preserves code if wrapped error is an `*Err` | Untrusted wrapped errors can carry misleading `Code` / `Op` context forward; op/msg text is not sanitised before later logging or presentation |
|
||||||
|
| `WrapCode(err error, code, op, msg string)` | `errors.go:86` | Caller-controlled `err`, `code`, `op`, `msg` | New `Err` with explicit code, then `(*Err).Error()` when rendered | Returns `nil` only when both `err == nil` and `code == ""` | Error-code spoofing; untrusted message/op text can be pushed into logs or external error responses |
|
||||||
|
| `NewCode(code, msg string)` | `errors.go:99` | Caller-controlled `code`, `msg` | New `Err` with no underlying error | None | Same as `WrapCode` without an underlying cause: spoofed codes and unsanitised user-facing text |
|
||||||
|
| `NewError(text string)` | `errors.go:119` | Caller-controlled text | `errors.New(text)` return value, then any downstream logging or response handling | None | Log / response injection or sensitive text disclosure when callers pass untrusted strings through as raw errors |
|
||||||
|
| `Join(errs ...error)` | `errors.go:125` | Caller-controlled error list | `errors.Join(errs...)`, then downstream `Error()` / logging | Standard library behaviour only | Size amplification and multi-message disclosure if attacker-controlled errors are aggregated and later surfaced verbatim |
|
||||||
|
| `LogError(err error, op, msg string)` | `errors.go:235` | Caller-controlled `err`, `op`, `msg`; global `defaultLogger` state | `Wrap(err, op, msg)` return value plus `defaultLogger.Error(msg, "op", op, "err", err)` -> `fmt.Fprintf` | Returns `nil` when `err == nil`; otherwise inherits logger controls (quoted string values, exact-key redaction) | Log injection through `msg`, `op`, or `err.Error()`; sensitive error disclosure; global DoS/exfiltration if `defaultLogger` was replaced or nil |
|
||||||
|
| `LogWarn(err error, op, msg string)` | `errors.go:250` | Caller-controlled `err`, `op`, `msg`; global `defaultLogger` state | `Wrap(err, op, msg)` return value plus `defaultLogger.Warn(msg, "op", op, "err", err)` -> `fmt.Fprintf` | Returns `nil` when `err == nil`; otherwise inherits logger controls | Same as `LogError`, with the additional risk that warnings may be treated as non-critical and reviewed less carefully |
|
||||||
|
| `Must(err error, op, msg string)` | `errors.go:265` | Caller-controlled `err`, `op`, `msg`; global `defaultLogger` state | `defaultLogger.Error(msg, "op", op, "err", err)` -> `fmt.Fprintf`, then `panic(Wrap(err, op, msg))` | Only executes when `err != nil`; otherwise same logger controls as `LogError` | Attacker-triggerable panic/DoS if external input can force the error path; same log injection and disclosure risks as `LogError` before the panic |
|
||||||
|
|
||||||
|
## Error Inspection Helpers
|
||||||
|
|
||||||
|
| Function | File:Line | Input source | Flows into | Current validation | Potential attack vector |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `(*Err).Unwrap()` | `errors.go:43` | Public `Err.Err` field | Returned underlying error | None | No direct sink in this package; risk comes from whatever the wrapped error does when callers continue processing it |
|
||||||
|
| `Is(err, target error)` | `errors.go:107` | Caller-controlled `err`, `target` | `errors.Is(err, target)` return value | Standard library behaviour only | Minimal direct attack surface here; any custom `Is` semantics come from attacker-supplied error implementations |
|
||||||
|
| `As(err error, target any)` | `errors.go:113` | Caller-controlled `err`, `target` | `errors.As(err, target)` return value | No wrapper-side validation of `target` | Invalid `target` values can panic through `errors.As`, creating an application-level DoS if misuse is reachable |
|
||||||
|
| `Op(err error)` | `errors.go:133` | Caller-controlled error chain | Extracted `Err.Op` string | Type-check via `errors.As`; otherwise none | Spoofed operation names if callers trust attacker-controlled `*Err` values for audit, metrics, or access decisions |
|
||||||
|
| `ErrCode(err error)` | `errors.go:143` | Caller-controlled error chain | Extracted `Err.Code` string | Type-check via `errors.As`; otherwise none | Spoofed machine-readable error codes if callers trust attacker-controlled wrapped errors |
|
||||||
|
| `Message(err error)` | `errors.go:153` | Caller-controlled error chain | Extracted `Err.Msg` or `err.Error()` string | Returns `""` for `nil`; otherwise none | Unsanitised attacker-controlled message text can be re-used in logs, UIs, or API responses |
|
||||||
|
| `Root(err error)` | `errors.go:166` | Caller-controlled error chain | Returned deepest unwrapped error | Returns `nil` for `nil`; otherwise none | Minimal direct sink, but can expose a sensitive root error that callers later surface verbatim |
|
||||||
|
| `AllOps(err error)` | `errors.go:181` | Caller-controlled error chain | Iterator over all `Err.Op` values | Traverses `errors.Unwrap` chain; ignores non-`*Err` nodes | Long attacker-controlled wrap chains can increase CPU work; extracted op strings remain unsanitised |
|
||||||
|
| `StackTrace(err error)` | `errors.go:198` | Caller-controlled error chain | `[]string` of operation names | None beyond `AllOps` traversal | Memory growth proportional to chain length; untrusted op strings may later be logged or displayed |
|
||||||
|
| `FormatStackTrace(err error)` | `errors.go:207` | Caller-controlled error chain | Joined stack string via `strings.Join(ops, " -> ")` | Returns `""` when no ops are found; otherwise none | Log / UI injection if untrusted op names are later rendered without escaping; size amplification for deep chains |
|
||||||
89
docs/api-contract.md
Normal file
89
docs/api-contract.md
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
---
|
||||||
|
title: API Contract
|
||||||
|
description: Exported API surface for dappco.re/go/core/log
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Contract
|
||||||
|
|
||||||
|
This page enumerates the exported API surface of `dappco.re/go/core/log`.
|
||||||
|
|
||||||
|
`Test coverage` means there is an explicit test in `log_test.go` or
|
||||||
|
`errors_test.go` that directly exercises the item. Indirect coverage through a
|
||||||
|
different exported API is marked `no`.
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
| Name | Signature | Package Path | Description | Test Coverage |
|
||||||
|
|------|-----------|--------------|-------------|---------------|
|
||||||
|
| `Err` | `type Err struct { Op string; Msg string; Err error; Code string }` | `dappco.re/go/core/log` | Structured error with operation context, message, wrapped cause, and optional machine-readable code. | yes |
|
||||||
|
| `Level` | `type Level int` | `dappco.re/go/core/log` | Logging verbosity enum ordered from quiet to debug. | yes |
|
||||||
|
| `Logger` | `type Logger struct { StyleTimestamp func(string) string; StyleDebug func(string) string; StyleInfo func(string) string; StyleWarn func(string) string; StyleError func(string) string; StyleSecurity func(string) string; /* unexported fields */ }` | `dappco.re/go/core/log` | Concurrent-safe structured logger with overridable style hooks. | yes |
|
||||||
|
| `Options` | `type Options struct { Level Level; Output io.Writer; Rotation *RotationOptions; RedactKeys []string }` | `dappco.re/go/core/log` | Logger construction options for level, destination, rotation, and redaction. | yes |
|
||||||
|
| `RotationOptions` | `type RotationOptions struct { Filename string; MaxSize int; MaxAge int; MaxBackups int; Compress bool }` | `dappco.re/go/core/log` | Log rotation and retention policy passed to the rotation writer factory. | yes |
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
| Name | Signature | Package Path | Description | Test Coverage |
|
||||||
|
|------|-----------|--------------|-------------|---------------|
|
||||||
|
| `LevelQuiet` | `const LevelQuiet Level = iota` | `dappco.re/go/core/log` | Suppresses all log output. | yes |
|
||||||
|
| `LevelError` | `const LevelError Level` | `dappco.re/go/core/log` | Emits only error and security messages. | yes |
|
||||||
|
| `LevelWarn` | `const LevelWarn Level` | `dappco.re/go/core/log` | Emits warnings, errors, and security messages. | yes |
|
||||||
|
| `LevelInfo` | `const LevelInfo Level` | `dappco.re/go/core/log` | Emits informational, warning, error, and security messages. | yes |
|
||||||
|
| `LevelDebug` | `const LevelDebug Level` | `dappco.re/go/core/log` | Emits all log levels, including debug details. | yes |
|
||||||
|
|
||||||
|
## Variable
|
||||||
|
|
||||||
|
| Name | Signature | Package Path | Description | Test Coverage |
|
||||||
|
|------|-----------|--------------|-------------|---------------|
|
||||||
|
| `RotationWriterFactory` | `var RotationWriterFactory func(RotationOptions) io.WriteCloser` | `dappco.re/go/core/log` | Optional injection point for creating rotating log writers. | yes |
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
| Name | Signature | Package Path | Description | Test Coverage |
|
||||||
|
|------|-----------|--------------|-------------|---------------|
|
||||||
|
| `(*Err).Error` | `func (e *Err) Error() string` | `dappco.re/go/core/log` | Formats an `Err` into its public error string representation. | yes |
|
||||||
|
| `(*Err).Unwrap` | `func (e *Err) Unwrap() error` | `dappco.re/go/core/log` | Returns the wrapped cause for `errors.Is` and `errors.As`. | yes |
|
||||||
|
| `(Level).String` | `func (l Level) String() string` | `dappco.re/go/core/log` | Returns the canonical lowercase name for a log level. | yes |
|
||||||
|
| `(*Logger).SetLevel` | `func (l *Logger) SetLevel(level Level)` | `dappco.re/go/core/log` | Updates the logger's active verbosity threshold. | yes |
|
||||||
|
| `(*Logger).Level` | `func (l *Logger) Level() Level` | `dappco.re/go/core/log` | Returns the logger's current verbosity threshold. | yes |
|
||||||
|
| `(*Logger).SetOutput` | `func (l *Logger) SetOutput(w io.Writer)` | `dappco.re/go/core/log` | Replaces the logger's output writer. | yes |
|
||||||
|
| `(*Logger).SetRedactKeys` | `func (l *Logger) SetRedactKeys(keys ...string)` | `dappco.re/go/core/log` | Replaces the set of keys whose values are redacted in output. | yes |
|
||||||
|
| `(*Logger).Debug` | `func (l *Logger) Debug(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Emits a debug log line when the logger level allows it. | yes |
|
||||||
|
| `(*Logger).Info` | `func (l *Logger) Info(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Emits an informational log line when the logger level allows it. | yes |
|
||||||
|
| `(*Logger).Warn` | `func (l *Logger) Warn(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Emits a warning log line when the logger level allows it. | yes |
|
||||||
|
| `(*Logger).Error` | `func (l *Logger) Error(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Emits an error log line when the logger level allows it. | yes |
|
||||||
|
| `(*Logger).Security` | `func (l *Logger) Security(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Emits a security log line using the security prefix at error visibility. | yes |
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
| Name | Signature | Package Path | Description | Test Coverage |
|
||||||
|
|------|-----------|--------------|-------------|---------------|
|
||||||
|
| `New` | `func New(opts Options) *Logger` | `dappco.re/go/core/log` | Constructs a logger from the supplied options and defaults. | yes |
|
||||||
|
| `Username` | `func Username() string` | `dappco.re/go/core/log` | Returns the current system username with environment fallbacks. | yes |
|
||||||
|
| `Default` | `func Default() *Logger` | `dappco.re/go/core/log` | Returns the package-level default logger. | yes |
|
||||||
|
| `SetDefault` | `func SetDefault(l *Logger)` | `dappco.re/go/core/log` | Replaces the package-level default logger. | yes |
|
||||||
|
| `SetLevel` | `func SetLevel(level Level)` | `dappco.re/go/core/log` | Updates the package-level default logger's level. | yes |
|
||||||
|
| `SetRedactKeys` | `func SetRedactKeys(keys ...string)` | `dappco.re/go/core/log` | Updates the package-level default logger's redaction keys. | yes |
|
||||||
|
| `Debug` | `func Debug(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Proxies a debug log call to the default logger. | yes |
|
||||||
|
| `Info` | `func Info(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Proxies an informational log call to the default logger. | yes |
|
||||||
|
| `Warn` | `func Warn(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Proxies a warning log call to the default logger. | yes |
|
||||||
|
| `Error` | `func Error(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Proxies an error log call to the default logger. | yes |
|
||||||
|
| `Security` | `func Security(msg string, keyvals ...any)` | `dappco.re/go/core/log` | Proxies a security log call to the default logger. | yes |
|
||||||
|
| `E` | `func E(op, msg string, err error) error` | `dappco.re/go/core/log` | Creates an `*Err` with operation context and an optional wrapped cause. | yes |
|
||||||
|
| `Wrap` | `func Wrap(err error, op, msg string) error` | `dappco.re/go/core/log` | Wraps an error with operation context and preserves any existing code. | yes |
|
||||||
|
| `WrapCode` | `func WrapCode(err error, code, op, msg string) error` | `dappco.re/go/core/log` | Wraps an error with operation context and an explicit machine-readable code. | yes |
|
||||||
|
| `NewCode` | `func NewCode(code, msg string) error` | `dappco.re/go/core/log` | Creates a coded error without an underlying cause. | yes |
|
||||||
|
| `Is` | `func Is(err, target error) bool` | `dappco.re/go/core/log` | Convenience wrapper around `errors.Is`. | yes |
|
||||||
|
| `As` | `func As(err error, target any) bool` | `dappco.re/go/core/log` | Convenience wrapper around `errors.As`. | yes |
|
||||||
|
| `NewError` | `func NewError(text string) error` | `dappco.re/go/core/log` | Convenience wrapper around `errors.New`. | yes |
|
||||||
|
| `Join` | `func Join(errs ...error) error` | `dappco.re/go/core/log` | Convenience wrapper around `errors.Join`. | yes |
|
||||||
|
| `Op` | `func Op(err error) string` | `dappco.re/go/core/log` | Extracts the outermost operation name from an `*Err`. | yes |
|
||||||
|
| `ErrCode` | `func ErrCode(err error) string` | `dappco.re/go/core/log` | Extracts the machine-readable code from an `*Err`. | yes |
|
||||||
|
| `Message` | `func Message(err error) string` | `dappco.re/go/core/log` | Returns an `*Err` message or falls back to `err.Error()`. | yes |
|
||||||
|
| `Root` | `func Root(err error) error` | `dappco.re/go/core/log` | Returns the deepest wrapped cause in an error chain. | yes |
|
||||||
|
| `AllOps` | `func AllOps(err error) iter.Seq[string]` | `dappco.re/go/core/log` | Returns an iterator over every non-empty operation in an error chain. | no |
|
||||||
|
| `StackTrace` | `func StackTrace(err error) []string` | `dappco.re/go/core/log` | Collects the logical stack trace of operations from an error chain. | yes |
|
||||||
|
| `FormatStackTrace` | `func FormatStackTrace(err error) string` | `dappco.re/go/core/log` | Formats the logical stack trace as a single `outer -> inner` string. | yes |
|
||||||
|
| `LogError` | `func LogError(err error, op, msg string) error` | `dappco.re/go/core/log` | Logs at error level and returns the wrapped error. | yes |
|
||||||
|
| `LogWarn` | `func LogWarn(err error, op, msg string) error` | `dappco.re/go/core/log` | Logs at warning level and returns the wrapped error. | yes |
|
||||||
|
| `Must` | `func Must(err error, op, msg string)` | `dappco.re/go/core/log` | Logs and panics when given a non-nil error. | yes |
|
||||||
74
docs/convention-drift-2026-03-23.md
Normal file
74
docs/convention-drift-2026-03-23.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Convention Drift Audit
|
||||||
|
|
||||||
|
Date: 2026-03-23
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- `CODEX.md` was not present anywhere under `/workspace` on 2026-03-23.
|
||||||
|
- This audit used `CLAUDE.md` as the available convention source.
|
||||||
|
- The `stdlib→core.*` bucket is interpreted here as explicit stdlib wording and non-`dappco.re/go/core/*` path references that still look pre-migration.
|
||||||
|
|
||||||
|
## Missing SPDX Headers
|
||||||
|
|
||||||
|
- `CLAUDE.md:1`
|
||||||
|
- `errors.go:1`
|
||||||
|
- `errors_test.go:1`
|
||||||
|
- `log.go:1`
|
||||||
|
- `log_test.go:1`
|
||||||
|
- `docs/index.md:1`
|
||||||
|
- `docs/development.md:1`
|
||||||
|
- `docs/architecture.md:1`
|
||||||
|
|
||||||
|
## stdlib→core.* Drift
|
||||||
|
|
||||||
|
- `CLAUDE.md:33` still describes `Is`, `As`, and `Join` as `stdlib wrappers`.
|
||||||
|
- `log.go:107` still references `core/go-io` rather than a `dappco.re/go/core/*` path.
|
||||||
|
- `docs/index.md:8` still uses the old module path `forge.lthn.ai/core/go-log`.
|
||||||
|
- `docs/index.md:18` still imports `forge.lthn.ai/core/go-log`.
|
||||||
|
- `docs/index.md:81` still describes runtime dependencies as `Go standard library only`.
|
||||||
|
- `docs/index.md:86` still references `core/go-io`.
|
||||||
|
- `docs/index.md:92` still lists the module path as `forge.lthn.ai/core/go-log`.
|
||||||
|
- `docs/index.md:95` still says `iter.Seq` comes from `the standard library`.
|
||||||
|
- `docs/development.md:10` still says `iter.Seq` comes from `the standard library`.
|
||||||
|
- `docs/development.md:126` still says `prefer the standard library`.
|
||||||
|
- `docs/architecture.md:217` still references `core/go-io`.
|
||||||
|
|
||||||
|
## UK English Drift
|
||||||
|
|
||||||
|
- `errors.go:264` uses `Initialize()` in an example comment.
|
||||||
|
- `log_test.go:179` uses `unauthorized access`.
|
||||||
|
- `log_test.go:185` asserts on `unauthorized access`.
|
||||||
|
- `errors_test.go:309` uses `initialization failed`.
|
||||||
|
|
||||||
|
## Missing Tests
|
||||||
|
|
||||||
|
- `errors.go:186` has no test for the `AllOps` early-stop path when the iterator consumer returns `false`.
|
||||||
|
- `log.go:289` has no test coverage for `Username()` falling back to `USER`.
|
||||||
|
- `log.go:292` has no test coverage for `Username()` falling back to `USERNAME` after `USER` is empty.
|
||||||
|
|
||||||
|
Coverage note: `go test -coverprofile=cover.out ./...` reports `97.7%` statement coverage; these are the only uncovered code paths in the current package.
|
||||||
|
|
||||||
|
## Missing Usage-Example Comments
|
||||||
|
|
||||||
|
Interpretation: public entry points without an inline `Example:` block in their doc comment, especially where peer APIs in `errors.go` already include examples.
|
||||||
|
|
||||||
|
- `log.go:71` `RotationOptions`
|
||||||
|
- `log.go:94` `Options`
|
||||||
|
- `log.go:108` `RotationWriterFactory`
|
||||||
|
- `log.go:111` `New`
|
||||||
|
- `log.go:157` `(*Logger).SetRedactKeys`
|
||||||
|
- `log.go:276` `(*Logger).Security`
|
||||||
|
- `log.go:284` `Username`
|
||||||
|
- `log.go:300` `Default`
|
||||||
|
- `log.go:305` `SetDefault`
|
||||||
|
- `errors.go:107` `Is`
|
||||||
|
- `errors.go:113` `As`
|
||||||
|
- `errors.go:119` `NewError`
|
||||||
|
- `errors.go:125` `Join`
|
||||||
|
- `errors.go:133` `Op`
|
||||||
|
- `errors.go:143` `ErrCode`
|
||||||
|
- `errors.go:153` `Message`
|
||||||
|
- `errors.go:166` `Root`
|
||||||
|
- `errors.go:181` `AllOps`
|
||||||
|
- `errors.go:198` `StackTrace`
|
||||||
|
- `errors.go:207` `FormatStackTrace`
|
||||||
13
errors.go
13
errors.go
|
|
@ -6,10 +6,9 @@
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"dappco.re/go/core"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"iter"
|
"iter"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Err represents a structured error with operational context.
|
// Err represents a structured error with operational context.
|
||||||
|
|
@ -29,14 +28,14 @@ func (e *Err) Error() string {
|
||||||
}
|
}
|
||||||
if e.Err != nil {
|
if e.Err != nil {
|
||||||
if e.Code != "" {
|
if e.Code != "" {
|
||||||
return fmt.Sprintf("%s%s [%s]: %v", prefix, e.Msg, e.Code, e.Err)
|
return core.Sprintf("%s%s [%s]: %v", prefix, e.Msg, e.Code, e.Err)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s%s: %v", prefix, e.Msg, e.Err)
|
return core.Sprintf("%s%s: %v", prefix, e.Msg, e.Err)
|
||||||
}
|
}
|
||||||
if e.Code != "" {
|
if e.Code != "" {
|
||||||
return fmt.Sprintf("%s%s [%s]", prefix, e.Msg, e.Code)
|
return core.Sprintf("%s%s [%s]", prefix, e.Msg, e.Code)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s%s", prefix, e.Msg)
|
return core.Sprintf("%s%s", prefix, e.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap returns the underlying error for use with errors.Is and errors.As.
|
// Unwrap returns the underlying error for use with errors.Is and errors.As.
|
||||||
|
|
@ -212,7 +211,7 @@ func FormatStackTrace(err error) string {
|
||||||
if len(ops) == 0 {
|
if len(ops) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return strings.Join(ops, " -> ")
|
return core.Join(" -> ", ops...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Combined Log-and-Return Helpers ---
|
// --- Combined Log-and-Return Helpers ---
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"dappco.re/go/core"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -311,7 +311,7 @@ func TestMust_Ugly_Panics(t *testing.T) {
|
||||||
|
|
||||||
// Verify error was logged before panic
|
// Verify error was logged before panic
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
assert.True(t, strings.Contains(output, "[ERR]") || len(output) > 0)
|
assert.True(t, core.Contains(output, "[ERR]") || len(output) > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStackTrace_Good(t *testing.T) {
|
func TestStackTrace_Good(t *testing.T) {
|
||||||
|
|
|
||||||
9
go.mod
9
go.mod
|
|
@ -2,13 +2,14 @@ module dappco.re/go/core/log
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.11.1
|
require (
|
||||||
|
dappco.re/go/core v0.6.0
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
7
go.sum
7
go.sum
|
|
@ -1,17 +1,14 @@
|
||||||
|
dappco.re/go/core v0.6.0 h1:0wmuO/UmCWXxJkxQ6XvVLnqkAuWitbd49PhxjCsplyk=
|
||||||
|
dappco.re/go/core v0.6.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
|
|
||||||
11
log.go
11
log.go
|
|
@ -6,6 +6,7 @@
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"dappco.re/go/core"
|
||||||
"fmt"
|
"fmt"
|
||||||
goio "io"
|
goio "io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -225,16 +226,16 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redaction logic
|
// Redaction logic
|
||||||
keyStr := fmt.Sprintf("%v", key)
|
keyStr := core.Sprintf("%v", key)
|
||||||
if slices.Contains(redactKeys, keyStr) {
|
if slices.Contains(redactKeys, keyStr) {
|
||||||
val = "[REDACTED]"
|
val = "[REDACTED]"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secure formatting to prevent log injection
|
// Secure formatting to prevent log injection
|
||||||
if s, ok := val.(string); ok {
|
if s, ok := val.(string); ok {
|
||||||
kvStr += fmt.Sprintf("%v=%q", key, s)
|
kvStr += core.Sprintf("%v=%q", key, s)
|
||||||
} else {
|
} else {
|
||||||
kvStr += fmt.Sprintf("%v=%v", key, val)
|
kvStr += core.Sprintf("%v=%v", key, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,10 +287,10 @@ func Username() string {
|
||||||
return u.Username
|
return u.Username
|
||||||
}
|
}
|
||||||
// Fallback for environments where user lookup might fail
|
// Fallback for environments where user lookup might fail
|
||||||
if u := os.Getenv("USER"); u != "" {
|
if u := core.Env("USER"); u != "" {
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
return os.Getenv("USERNAME")
|
return core.Env("USERNAME")
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Default logger ---
|
// --- Default logger ---
|
||||||
|
|
|
||||||
48
log_test.go
48
log_test.go
|
|
@ -2,8 +2,8 @@ package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"dappco.re/go/core"
|
||||||
goio "io"
|
goio "io"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -12,6 +12,10 @@ type nopWriteCloser struct{ goio.Writer }
|
||||||
|
|
||||||
func (nopWriteCloser) Close() error { return nil }
|
func (nopWriteCloser) Close() error { return nil }
|
||||||
|
|
||||||
|
func substringCount(s, substr string) int {
|
||||||
|
return len(core.Split(s, substr)) - 1
|
||||||
|
}
|
||||||
|
|
||||||
func TestLogger_Levels(t *testing.T) {
|
func TestLogger_Levels(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -69,13 +73,13 @@ func TestLogger_KeyValues(t *testing.T) {
|
||||||
l.Info("test message", "key1", "value1", "key2", 42)
|
l.Info("test message", "key1", "value1", "key2", 42)
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
if !strings.Contains(output, "test message") {
|
if !core.Contains(output, "test message") {
|
||||||
t.Error("expected message in output")
|
t.Error("expected message in output")
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "key1=\"value1\"") {
|
if !core.Contains(output, "key1=\"value1\"") {
|
||||||
t.Errorf("expected key1=\"value1\" in output, got %q", output)
|
t.Errorf("expected key1=\"value1\" in output, got %q", output)
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "key2=42") {
|
if !core.Contains(output, "key2=42") {
|
||||||
t.Error("expected key2=42 in output")
|
t.Error("expected key2=42 in output")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,10 +94,10 @@ func TestLogger_ErrorContext(t *testing.T) {
|
||||||
l.Error("something failed", "err", err)
|
l.Error("something failed", "err", err)
|
||||||
|
|
||||||
got := buf.String()
|
got := buf.String()
|
||||||
if !strings.Contains(got, "op=\"outer.Op\"") {
|
if !core.Contains(got, "op=\"outer.Op\"") {
|
||||||
t.Errorf("expected output to contain op=\"outer.Op\", got %q", got)
|
t.Errorf("expected output to contain op=\"outer.Op\", got %q", got)
|
||||||
}
|
}
|
||||||
if !strings.Contains(got, "stack=\"outer.Op -> test.Op\"") {
|
if !core.Contains(got, "stack=\"outer.Op -> test.Op\"") {
|
||||||
t.Errorf("expected output to contain stack=\"outer.Op -> test.Op\", got %q", got)
|
t.Errorf("expected output to contain stack=\"outer.Op -> test.Op\", got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,13 +113,13 @@ func TestLogger_Redaction(t *testing.T) {
|
||||||
l.Info("login", "user", "admin", "password", "secret123", "token", "abc-123")
|
l.Info("login", "user", "admin", "password", "secret123", "token", "abc-123")
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
if !strings.Contains(output, "user=\"admin\"") {
|
if !core.Contains(output, "user=\"admin\"") {
|
||||||
t.Error("expected user=\"admin\"")
|
t.Error("expected user=\"admin\"")
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "password=\"[REDACTED]\"") {
|
if !core.Contains(output, "password=\"[REDACTED]\"") {
|
||||||
t.Errorf("expected password=\"[REDACTED]\", got %q", output)
|
t.Errorf("expected password=\"[REDACTED]\", got %q", output)
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "token=\"[REDACTED]\"") {
|
if !core.Contains(output, "token=\"[REDACTED]\"") {
|
||||||
t.Errorf("expected token=\"[REDACTED]\", got %q", output)
|
t.Errorf("expected token=\"[REDACTED]\", got %q", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,11 +131,11 @@ func TestLogger_InjectionPrevention(t *testing.T) {
|
||||||
l.Info("message", "key", "value\n[SEC] injected message")
|
l.Info("message", "key", "value\n[SEC] injected message")
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
if !strings.Contains(output, "key=\"value\\n[SEC] injected message\"") {
|
if !core.Contains(output, "key=\"value\\n[SEC] injected message\"") {
|
||||||
t.Errorf("expected escaped newline, got %q", output)
|
t.Errorf("expected escaped newline, got %q", output)
|
||||||
}
|
}
|
||||||
// Ensure it's still a single line (excluding trailing newline)
|
// Ensure it's still a single line (excluding trailing newline)
|
||||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
lines := core.Split(core.Trim(output), "\n")
|
||||||
if len(lines) != 1 {
|
if len(lines) != 1 {
|
||||||
t.Errorf("expected 1 line, got %d", len(lines))
|
t.Errorf("expected 1 line, got %d", len(lines))
|
||||||
}
|
}
|
||||||
|
|
@ -179,13 +183,13 @@ func TestLogger_Security(t *testing.T) {
|
||||||
l.Security("unauthorized access", "user", "admin")
|
l.Security("unauthorized access", "user", "admin")
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
if !strings.Contains(output, "[SEC]") {
|
if !core.Contains(output, "[SEC]") {
|
||||||
t.Error("expected [SEC] prefix in security log")
|
t.Error("expected [SEC] prefix in security log")
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "unauthorized access") {
|
if !core.Contains(output, "unauthorized access") {
|
||||||
t.Error("expected message in security log")
|
t.Error("expected message in security log")
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "user=\"admin\"") {
|
if !core.Contains(output, "user=\"admin\"") {
|
||||||
t.Error("expected context in security log")
|
t.Error("expected context in security log")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -212,14 +216,14 @@ func TestLogger_SetRedactKeys_Good(t *testing.T) {
|
||||||
|
|
||||||
// No redaction initially
|
// No redaction initially
|
||||||
l.Info("msg", "secret", "visible")
|
l.Info("msg", "secret", "visible")
|
||||||
if !strings.Contains(buf.String(), "secret=\"visible\"") {
|
if !core.Contains(buf.String(), "secret=\"visible\"") {
|
||||||
t.Errorf("expected visible value, got %q", buf.String())
|
t.Errorf("expected visible value, got %q", buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
l.SetRedactKeys("secret")
|
l.SetRedactKeys("secret")
|
||||||
l.Info("msg", "secret", "hidden")
|
l.Info("msg", "secret", "hidden")
|
||||||
if !strings.Contains(buf.String(), "secret=\"[REDACTED]\"") {
|
if !core.Contains(buf.String(), "secret=\"[REDACTED]\"") {
|
||||||
t.Errorf("expected redacted value, got %q", buf.String())
|
t.Errorf("expected redacted value, got %q", buf.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +235,7 @@ func TestLogger_OddKeyvals_Good(t *testing.T) {
|
||||||
// Odd number of keyvals — last key should have no value
|
// Odd number of keyvals — last key should have no value
|
||||||
l.Info("msg", "lonely_key")
|
l.Info("msg", "lonely_key")
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
if !strings.Contains(output, "lonely_key=<nil>") {
|
if !core.Contains(output, "lonely_key=<nil>") {
|
||||||
t.Errorf("expected lonely_key=<nil>, got %q", output)
|
t.Errorf("expected lonely_key=<nil>, got %q", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,10 +249,10 @@ func TestLogger_ExistingOpNotDuplicated_Good(t *testing.T) {
|
||||||
l.Error("failed", "op", "explicit.Op", "err", err)
|
l.Error("failed", "op", "explicit.Op", "err", err)
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
if strings.Count(output, "op=") != 1 {
|
if substringCount(output, "op=") != 1 {
|
||||||
t.Errorf("expected exactly one op= in output, got %q", output)
|
t.Errorf("expected exactly one op= in output, got %q", output)
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "op=\"explicit.Op\"") {
|
if !core.Contains(output, "op=\"explicit.Op\"") {
|
||||||
t.Errorf("expected explicit op, got %q", output)
|
t.Errorf("expected explicit op, got %q", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,10 +266,10 @@ func TestLogger_ExistingStackNotDuplicated_Good(t *testing.T) {
|
||||||
l.Error("failed", "stack", "custom.Stack", "err", err)
|
l.Error("failed", "stack", "custom.Stack", "err", err)
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
if strings.Count(output, "stack=") != 1 {
|
if substringCount(output, "stack=") != 1 {
|
||||||
t.Errorf("expected exactly one stack= in output, got %q", output)
|
t.Errorf("expected exactly one stack= in output, got %q", output)
|
||||||
}
|
}
|
||||||
if !strings.Contains(output, "stack=\"custom.Stack\"") {
|
if !core.Contains(output, "stack=\"custom.Stack\"") {
|
||||||
t.Errorf("expected custom stack, got %q", output)
|
t.Errorf("expected custom stack, got %q", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +336,7 @@ func TestDefault_Good(t *testing.T) {
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
for _, tag := range []string{"[DBG]", "[INF]", "[WRN]", "[ERR]", "[SEC]"} {
|
for _, tag := range []string{"[DBG]", "[INF]", "[WRN]", "[ERR]", "[SEC]"} {
|
||||||
if !strings.Contains(output, tag) {
|
if !core.Contains(output, tag) {
|
||||||
t.Errorf("expected %s in output, got %q", tag, output)
|
t.Errorf("expected %s in output, got %q", tag, output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue