sugarcraft / candy-log
PHP port of charmbracelet/log — minimal, colorful leveled logging with structured human-readable output, text/JSON/logfmt formatters, and stdlog adapter.
Requires
- php: ^8.3
- psr/log: ^3.0
- sugarcraft/candy-core: dev-master
- sugarcraft/candy-palette: dev-master
- sugarcraft/candy-sprinkles: dev-master
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-07-10 03:49:10 UTC
README
CandyLog
PHP port of charmbracelet/log — a minimal, colorful leveled logging library.
Features
- Leveled logging —
Debug,Info,Warn,Error,Fatallevels - Colorful human-readable output — terminal-styled by default (Probe-driven: respects
NO_COLOR/FORCE_COLOR) - Multiple formatters —
TextFormatter(default),JSONFormatter,LogfmtFormatter - Structured key/value pairs — pass arbitrary context with every log call
- Sub-loggers —
with([...])creates a child logger with persistent fields - Per-field styling —
Styles::keysmaps field names to their ANSI styles - syslog-aligned levels — integer values (-4/0/4/8/12) for easy threshold filtering
- stdlog adapter — wrap in
Log\StandardLogAdapterfor*log.Loggerinterface compatibility - PSR-3 bridge —
PsrBridgewrapper provides full PSR-3LoggerInterfacemethods - Hook system — register callbacks per log level via
HookRegistry::onLevel() - Configurable log-part ordering —
PartsOrderDTO controls which parts appear and in what sequence
Install
composer require sugarcraft/candy-log
Quick Start
use SugarCraft\Log\Logger; use SugarCraft\Log\Level; $log = Logger::new(); $log->info('Starting oven', ['degree' => 375]); $log->warn('Almost ready', ['batch' => 2]); $log->error('Temperature too low', ['err' => 'underheated']);
Levels
Levels are syslog-aligned integers — use ->value for threshold comparisons:
Level::Debug->value; // -4 Level::Info->value; // 0 Level::Warn->value; // 4 Level::Error->value; // 8 Level::Fatal->value; // 12 $log->info('info message'); $log->warn('warn message'); $log->error('error message'); $log->fatal('fatal message'); // throws RuntimeException $log->print('always prints'); // no level prefix
Structured Fields
$log->info('Baking cookies', [ 'flour' => '2 cups', 'butter' => true, 'temp' => 375, ]); // Child logger with persistent fields $baker = $log->with(['user' => 'chef', 'session' => 'am']); $baker->info('Batch started'); // also has user + session
Formatters
use SugarCraft\Log\Formatter\TextFormatter; use SugarCraft\Log\Formatter\JsonFormatter; use SugarCraft\Log\Formatter\LogfmtFormatter; $log = Logger::new(formatter: new JsonFormatter());
Styling
Styles are applied automatically when the terminal supports color output.
Color is determined by candy-palette's Probe — it respects the NO_COLOR
and FORCE_COLOR environment variables.
Override level styles via Logger::styles():
use SugarCraft\Sprinkles\Style; $log = Logger::new(); $styles = $log->styles(); $styles->levels[Level::Error->value] = Style::new()->foreground('red')->bold(); $log->setStyles($styles);
Per-field styles
Styles::keys maps field names (time, level, prefix, caller,
message, key, value) to individual Style objects:
$styles = $log->styles(); $styles->keys['time'] = Style::new()->foreground('cyan'); $styles->keys['caller'] = Style::new()->foreground('grey'); $log->setStyles($styles);
Level text alignment
Styles::padLevelText($label) right-pads a level label to 5 characters for
column-aligned log output:
Styles::padLevelText('INFO'); // "INFO " Styles::padLevelText('DEBUG'); // "DEBUG"
Panic Handlers
use SugarCraft\Log\Log; // Install a panic handler that catches uncaught exceptions and fatal errors, // restores the terminal from altscreen mode, and prints a styled panic report. Log::installPanicHandler(); // Restore terminal state manually (exit altscreen, show cursor). // Called automatically by the panic handler, but safe to call directly. Log::restoreTerminal();
The panic handler catches uncaught exceptions and fatal errors (E_ERROR, E_PARSE), restores the terminal to a usable state, and prints a colorized banner with the exception class, message, and backtrace.
PSR-3 Bridge
PsrBridge wraps a Logger instance and provides the full PSR-3 LoggerInterface API (emergency, alert, critical, error, warning, notice, info, debug, log). Use it anywhere a PSR-3 logger is expected:
use SugarCraft\Log\Logger; use SugarCraft\Log\PsrBridge; use Psr\Log\LogLevel; $logger = new Logger(); $psr = new PsrBridge($logger); // All PSR-3 methods available $psr->warning('Something is off', ['detail' => 'temperature rising']); $psr->log(LogLevel::ERROR, 'Operation failed', ['code' => 500]);
The bridge also fires registered hooks before forwarding each message to the underlying logger, enabling middleware-style interceptors.
Hook System
The hook system lets you register callbacks that fire whenever a log entry is emitted at or above a given level. Hooks receive the Level, PSR-3 level string, message, and context — useful for dispatching to external services, enriching context, or filtering.
Note: Hooks fire only via
PsrBridge— they do not fire when callingLogger->info()etc. directly. For the plainLoggerAPI, hooks are not invoked. To use hooks, wrap yourLoggerin aPsrBridgeand call methods on the bridge.
use SugarCraft\Log\Logger; use SugarCraft\Log\Level; use SugarCraft\Log\Hook\HookRegistry; $logger = new Logger(); $hooks = new HookRegistry(); // Register a callback for all Warn-and-above entries $id = $hooks->onLevel(Level::Warn, function (Level $level, string $psrLevel, string $message, array $context) { // Dispatch to external service, enrich context, etc. file_put_contents('/tmp/warn.log', "[{$level->label()}] {$message}\n", FILE_APPEND); }); // Pass hooks to the PsrBridge, or fire them manually $hooks->fire(Level::Warn, 'warning', 'Something is off', []);
HookRegistry::onLevel(Level, callable) returns a registration ID. HookRegistry::fire(Level, psrLevel, message, context) dispatches to all handlers whose minimum level is met.
The Hook interface is also available for structured implementations:
use SugarCraft\Log\Hook\Hook; use SugarCraft\Log\Level; final class MetricsHook implements Hook { public function onLevel(Level $level, string $psrLevel, string $message, array $context): void { // Ship metrics to your observability platform } }
Parts Order
PartsOrder is a config DTO that controls which log-parts appear and in what sequence when formatting. It ships with three named presets:
use SugarCraft\Log\PartsOrder; // Default: timestamp level prefix? caller? message fields? PartsOrder::default(); // [timestamp, level, prefix, caller, message, fields] // Syslog-friendly: omits prefix and caller PartsOrder::syslog(); // [timestamp, level, message, fields] // Message-first: message comes before level and timestamp PartsOrder::messageFirst(); // [message, level, timestamp, fields] // Custom ordering $order = new PartsOrder([PartsOrder::PART_MESSAGE, PartsOrder::PART_LEVEL, PartsOrder::PART_FIELDS]); // Query whether a part is included $order->has(PartsOrder::PART_CALLER); // false for syslog(), true for default()
Named part constants: PART_TIMESTAMP, PART_LEVEL, PART_PREFIX, PART_CALLER, PART_MESSAGE, PART_FIELDS.
Caller Information
CallerFormatter::find() walks the call stack and returns "file:line" of the first frame outside the log package — the true call site:
use SugarCraft\Log\CallerFormatter; $caller = CallerFormatter::find(); // e.g. "my-script.php:42"
Used internally by formatters when $reportCaller is enabled on the Logger.