| name | logbox |
|---|---|
| description | Use this skill whenever working with LogBox -- standalone or inside a ColdBox application. Covers installation, configuration DSL (appenders, root logger, categories), all built-in appender types with their properties, logger injection via WireBox DSL, category inheritance, structured logging with extraInfo, performance patterns (can{} methods, closure messages), custom appenders, custom layouts, async logging, environment-specific config, and production best practices. |
| applyTo | **/*.{bx,cfc,cfm,bxm} |
Load this skill when:
- Installing or configuring LogBox (standalone or in ColdBox)
- Choosing and configuring appenders (console, file, rolling, DB, scope, socket, email)
- Setting up categories, the root logger, or category-level inheritance
- Injecting loggers into services, handlers, or interceptors via WireBox
- Using structured
extraInfologging or implementing$toString() - Writing performant logging using
canDebug()/ closure messages - Building custom appenders or layouts
- Configuring different log levels per environment
Examples default to BoxLang (.bx) syntax. Adapt as needed:
| Concept | BoxLang (.bx) |
CFML (.cfc) |
|---|---|---|
| Class declaration | class [extends="..."] { |
component [extends="..."] { |
| DI annotation | @inject above property |
property name="x" inject="y"; |
| Template suffix | .bxm |
.cfm / .cfml |
| Tag prefix | <bx:...> |
<cf...> |
CFML Compat: BoxLang + CFML Compat module lets
.bxand.cfcfiles coexist. Useclass {}for BoxLang-native,component {}for CFML-compat.
LogBox has five components that work together:
- LogBox — central library storing loggers, appenders, categories, and configuration
- Logger — sends messages to appropriate destinations according to its category
- Categories — each logger has a unique category, typically a dot-notation class path
- Appenders — components that receive log events and write them to a destination
- Layouts — control the message format sent by an appender
Levels run from most severe (0) to most verbose (5):
| Level | Int | When to Use |
|---|---|---|
FATAL |
0 | Severe error — application may not recover |
ERROR |
1 | Error condition — app continues |
WARN |
2 | Unexpected but non-breaking situation |
INFO |
3 | Significant business events |
DEBUG |
4 | Diagnostic info useful during development |
OFF |
5 | Disable all logging for a category |
levelMin/levelMaxdefine the window of levels an appender or category accepts. Use numeric integers or string names interchangeably.
LogBox is bundled with ColdBox. Configure it in config/LogBox.cfc (by convention) or inline in config/ColdBox.cfc.
# Install via CommandBox
box install logboxAdd the mapping in Application.cfc:
this.mappings[ "/logbox" ] = expandPath( "/path/to/logbox" );
Standalone namespace: logbox.system.logging
ColdBox namespace: coldbox.system.logging
Verify standalone installation:
logbox = new logbox.system.logging.LogBox();
The same DSL works in all three contexts:
config/LogBox.cfc— standalone CFC withconfigure()methodconfig/ColdBox.cfclogboxvariable — inline ColdBox config- Struct literal passed to the
LogBoxconstructor (standalone)
logBox = {
appenders : {}, // Required: one or more appender definitions
root : {}, // Required: root logger settings
categories : {}, // Optional: named category overrides (granular)
fatal : [], // Optional: array of category names → FATAL only
error : [], // Optional: array of category names → ERROR only
warn : [], // Optional: array of category names → WARN only
info : [], // Optional: array of category names → INFO only
debug : [], // Optional: array of category names → DEBUG only
off : [] // Optional: array of category names → logging off
}
| Key | Required | Description |
|---|---|---|
class |
Yes | Full class path of the appender |
properties |
No | Struct of appender-specific settings |
layout |
No | Class path of a custom layout |
levelMin |
No | Min level (defaults to 0 / FATAL) |
levelMax |
No | Max level (defaults to 4 / DEBUG) |
async |
No | Boolean — log asynchronously in a separate thread (default: false) |
| Key | Required | Description |
|---|---|---|
levelMin |
No | Min severity (defaults to FATAL) |
levelMax |
No | Max severity (defaults to DEBUG) |
appenders |
Yes | Comma-list of appender names or "*" for all |
exclude |
No | Comma-list of appenders to exclude |
Same as root logger, plus name. When no config is found for a requested category, LogBox walks up the dot-notation hierarchy to the first ancestor that is configured, then falls back to root.
// BoxLang
class extends="logbox.system.logging.config.LogBoxConfig" {
function configure() {
appenders = {
console : {
class : "logbox.system.logging.appenders.ConsoleAppender",
levelMin : "DEBUG",
levelMax : "FATAL"
},
fileApp : {
class : "logbox.system.logging.appenders.FileAppender",
levelMin : "INFO",
levelMax : "FATAL",
properties : {
filePath : "/app/logs",
fileName : "application",
autoExpand : false
}
},
rollingApp : {
class : "logbox.system.logging.appenders.RollingFileAppender",
levelMin : "WARN",
levelMax : "FATAL",
properties : {
filePath : "/app/logs",
fileName : "app",
autoExpand : false,
fileMaxSize : 5000,
fileMaxArchives : 5
}
}
}
root = {
levelMin : "WARN",
levelMax : "FATAL",
appenders : "rollingApp"
}
categories = {
"models" : { levelMin : "DEBUG", levelMax : "INFO", appenders : "console,fileApp" },
"models.UserService" : { levelMin : "DEBUG", levelMax : "DEBUG", appenders : "console" }
}
// Shorthand: put these categories at DEBUG level using root appenders
debug = [ "coldbox.system.interceptors" ]
// Silence noisy third-party packages
off = [ "coldbox.system.plugins.BeanFactory" ]
}
}
// CFML equivalent
component extends="coldbox.system.logging.config.LogBoxConfig" {
function configure() {
appenders = {
console : {
class : "coldbox.system.logging.appenders.ConsoleAppender",
levelMin : "DEBUG",
levelMax : "FATAL"
},
rollingApp : {
class : "coldbox.system.logging.appenders.RollingFileAppender",
levelMin : "WARN",
levelMax : "FATAL",
properties : {
filePath : "/app/logs",
fileName : "app",
autoExpand : false,
fileMaxSize : 5000,
fileMaxArchives : 5
}
}
}
root = { levelMin : "WARN", levelMax : "FATAL", appenders : "rollingApp" }
categories = {
"models" : { levelMin : "DEBUG", levelMax : "INFO", appenders : "console" }
}
}
}// Inside configure() — shared across all environments
logbox = {
appenders : {
console : {
class : "coldbox.system.logging.appenders.ConsoleAppender",
levelMin : "DEBUG",
levelMax : "FATAL"
},
rolling : {
class : "coldbox.system.logging.appenders.RollingFileAppender",
levelMin : "WARN",
levelMax : "FATAL",
properties : {
filePath : "#getDirectoryFromPath( getBaseTemplatePath() )#logs",
fileName : "app",
fileMaxSize : 5000,
fileMaxArchives : 3
}
}
},
root : { levelMin : "WARN", levelMax : "FATAL", appenders : "rolling" },
categories : {}
}
// Development override — verbose, no file I/O
function development() {
logbox.root = { levelMin : "DEBUG", levelMax : "FATAL", appenders : "console" }
}
// Production override — only warnings and above to rolling file
function production() {
logbox.root = { levelMin : "WARN", levelMax : "FATAL", appenders : "rolling" }
}
// In config/ColdBox.cfc — point to an external file
logbox = { configFile : "config/LogBox" }
// or with a package path
logbox = { configFile : "myapp.config.LogBox" }
// Direct struct config (no CFC needed)
var logbox = new logbox.system.logging.LogBox( {
appenders : {
console : { class : "logbox.system.logging.appenders.ConsoleAppender" }
},
root : { levelMax : "DEBUG", appenders : "*" }
} )
var log = logbox.getLogger( "myapp.services.UserService" )
log.info( "LogBox running standalone" )
Use the short class name (relative to coldbox.system.logging.appenders. or logbox.system.logging.appenders.) or the full path.
Writes to the system console/stdout. Ideal for development and Docker containers.
console : {
class : "coldbox.system.logging.appenders.ConsoleAppender"
// No required properties
}
Writes to a single log file (appends forever — no rotation).
fileApp : {
class : "coldbox.system.logging.appenders.FileAppender",
properties : {
filePath : "/app/logs", // Required — directory path
fileName : "application", // Optional — defaults to appender name
fileEncoding : "UTF-8", // Optional — default UTF-8
autoExpand : false // Set false for absolute paths
}
}
Always set
autoExpand : falsewhen using absolute file paths.
Writes to a file and automatically rotates when the file exceeds a size limit.
rollingApp : {
class : "coldbox.system.logging.appenders.RollingFileAppender",
properties : {
filePath : "/app/logs", // Required
fileName : "app", // Optional
fileEncoding : "UTF-8", // Optional
autoExpand : false, // Set false for absolute paths
fileMaxSize : 2000, // KB — default 2000 (2 MB)
fileMaxArchives : 2 // Number of archived files to keep — default 2
}
}
Writes log entries to a database table.
dbLog : {
class : "coldbox.system.logging.appenders.DBAppender",
levelMin : "ERROR",
levelMax : "FATAL",
properties : {
dsn : "myDSN", // Required
table : "app_logs", // Required
autocreate : true, // Create table if not exists — default false
rotate : true, // Delete old records — default true
rotationDays : 30 // Days to keep records — default 30
}
}
Table columns (when autocreate : true): id (UUID), severity, category, logdate, appendername, message, extrainfo.
Use columnMap to alias your own column names:
properties : {
dsn : "myDSN",
table : "logs",
columnMap : {
severity : "log_level",
category : "log_category",
message : "log_message",
extrainfo : "log_extra"
}
}
Stores log entries in a CF/BoxLang scope (useful for debugging in-request).
scopeLog : {
class : "coldbox.system.logging.appenders.ScopeAppender",
properties : {
scope : "request", // Any valid scope — default "request"
key : "logs", // Key within the scope — default appender name
limit : 50 // Max entries to keep (0 = unlimited, default)
}
}
Transmits log events over a TCP socket.
socketApp : {
class : "coldbox.system.logging.appenders.SocketAppender",
properties : {
host : "logserver.example.com",
port : 5000,
timeout : 3,
persistConnection : false
}
}
Sends log events via email (use sparingly — FATAL/ERROR only).
emailAlert : {
class : "coldbox.system.logging.appenders.EmailAppender",
levelMin : "FATAL",
levelMax : "ERROR",
properties : {
from : "errors@example.com",
to : "ops@example.com",
subject : "[PROD] Application Error"
}
}
Any appender can log asynchronously. Add async : true to its config:
fileApp : {
class : "coldbox.system.logging.appenders.FileAppender",
async : true,
properties : { filePath : "/app/logs" }
}
The logbox: DSL prefix is available in any WireBox-managed component.
| DSL Token | Returns |
|---|---|
logbox |
The LogBox instance itself |
logbox:root |
The root logger |
logbox:logger:{this} |
Logger whose category = current class path |
logbox:logger:some.category |
Logger for a specific named category |
// BoxLang
class UserService {
// Best practice: category auto-matches class path
@inject( "logbox:logger:{this}" )
property name="log";
// Access the LogBox instance itself
@inject( "logbox" )
property name="logBox";
}
// CFML
component {
property name="log" inject="logbox:logger:{this}";
property name="logBox" inject="logbox";
}| Method | Description |
|---|---|
fatal( message [, extraInfo] ) |
Log at FATAL level |
error( message [, extraInfo] ) |
Log at ERROR level |
warn( message [, extraInfo] ) |
Log at WARN level |
info( message [, extraInfo] ) |
Log at INFO level |
debug( message [, extraInfo] ) |
Log at DEBUG level |
logMessage( message, severity [, extraInfo] ) |
Log at any level |
message can be a string or a closure (see performance section). extraInfo can be any value: string, struct, query, array, object.
| Method | Description |
|---|---|
canLog( level ) |
Returns true if the given numeric level is loggable |
canFatal() / canError() / canWarn() / canInfo() / canDebug() |
Shorthand level checks |
getLevelMin() / getLevelMax() |
Get configured level bounds |
setLevelMin( level ) / setLevelMax( level ) |
Override levels at runtime |
getRootLogger() |
Get the root logger |
getCategory() |
Get this logger's category name |
| Method | Description |
|---|---|
getLogger( category ) |
Get a logger by category string or pass this |
getRootLogger() |
Get the root logger |
configure( config ) |
Dynamically reconfigure LogBox at runtime |
getCurrentAppenders() |
List of registered appender names |
getCurrentLoggers() |
List of instantiated logger names |
class OrderService {
@inject( "logbox:logger:{this}" )
property name="log";
function processOrder( required order ) {
log.info( "Processing order ##order.getId()" )
try {
var result = paymentGateway.charge( order )
log.info( "Payment successful", { transactionId : result.id, amount : result.amount } )
return result
} catch( PaymentException e ) {
log.error( "Payment failed for order ##order.getId()", e )
rethrow
}
}
}
component {
property name="log" inject="logbox:logger:{this}";
function processOrder( required order ) {
log.info( "Processing order ##order.getId()" )
try {
var result = paymentGateway.charge( order )
log.info( "Payment successful", { transactionId : result.id, amount : result.amount } )
return result
} catch( PaymentException e ) {
log.error( "Payment failed for order ##order.getId()", e )
rethrow
}
}
}// Get logger by string category
var log = application.logbox.getLogger( "myapp.services.OrderService" )
// Get logger scoped to the current object (resolves to full CFC path)
var log = application.logbox.getLogger( this )
// BAD: the string interpolation runs even if DEBUG is disabled
log.debug( "User data: #serializeJSON( user )#" )
if( log.canDebug() ) {
log.debug( "User data: #serializeJSON( user )#" )
}
// The closure is ONLY executed when the level is enabled
log.debug( () => "User data: #serializeJSON( user )#" )
log.debug( () => {
var parts = []
parts.append( "id=#user.getId()#" )
parts.append( "email=#user.getEmail()#" )
return parts.toList( ", " )
} )
Always use canDebug() or closure messages before any expensive serialization, query, or computation that exists solely for logging.
extraInfo accepts any value. Appenders serialize it using this algorithm:
- Simple value → use as-is
- Object with
$toString()→ call it and use the return string - Exception → serialize to JSON
- Object without
$toString()→ serialize to XML - Struct / array / query → serialize to JSON
class User {
function $toString() {
return "#getName()#,#getEmail()#,#getRole()#"
}
}
component {
function $toString() {
return "#getName()#,#getEmail()#,#getRole()#"
}
}// When logged, calls user.$toString() automatically
log.debug( "User authenticated", user )
// Structured struct — serialized to JSON
log.error( "Checkout failed", {
userId : user.getId(),
cart : cart.getTotalItems(),
error : e.message
} )
Categories use dot-notation inheritance. When a logger for a.b.c is requested and not configured, LogBox walks up: a.b → a → root.
root FATAL–DEBUG → console, file
└── models INFO–DEBUG → console
└── models.UserService DEBUG only → console (explicitly configured)
└── models.OrderService (NOT configured) → inherits from models: INFO–DEBUG → console
Example configuration:
categories = {
// All models package — INFO and above
"models" : { levelMin : "INFO", levelMax : "FATAL", appenders : "console" },
// One specific model — DEBUG only
"models.UserService" : { levelMin : "DEBUG", levelMax : "DEBUG", appenders : "console" }
}
models.OrderService is not configured → it inherits from models (INFO–FATAL, console).
models.UserService is explicitly configured → DEBUG only, console.
Use package-level categories to toggle entire features without changing code.
Extend AbstractAppender (standalone: logbox.system.logging.AbstractAppender, ColdBox: coldbox.system.logging.AbstractAppender).
// BoxLang — models/logging/WebhookAppender.bx
class WebhookAppender extends coldbox.system.logging.AbstractAppender {
function init( required string name, struct properties = {}, string layout = "", numeric levelMin = 0, numeric levelMax = 4 ) {
super.init( argumentCollection = arguments )
return this
}
// Called once when appender is registered — good for setup
function onRegistration() {
variables.webhookUrl = getProperty( "webhookUrl" )
variables.minSeverity = getProperty( "minSeverity", "ERROR" )
}
// Called for every log event this appender receives
function logMessage( required logEvent ) {
if( !len( variables.webhookUrl ) ) return
// Use hasCustomLayout() + getCustomLayout() for custom formatting
var message = hasCustomLayout()
? getCustomLayout().format( logEvent )
: "[#severityToString( logEvent.getSeverity() )#] #logEvent.getMessage()#"
http url=variables.webhookUrl method="POST" throwOnError=false {
httpparam type="header" name="Content-Type" value="application/json"
httpparam type="body" value=serializeJSON( {
severity : severityToString( logEvent.getSeverity() ),
category : logEvent.getCategory(),
message : message,
extraInfo : logEvent.getExtraInfo(),
timestamp : logEvent.getTimestamp()
} )
}
}
function onUnRegistration() {}
}
// CFML equivalent
component extends="coldbox.system.logging.AbstractAppender" {
function init( required string name, struct properties = {}, string layout = "", numeric levelMin = 0, numeric levelMax = 4 ) {
super.init( argumentCollection = arguments )
return this
}
function onRegistration() {
variables.webhookUrl = getProperty( "webhookUrl" )
}
function logMessage( required logEvent ) {
if( !len( variables.webhookUrl ) ) return
cfhttp( url=variables.webhookUrl, method="POST", throwOnError=false ) {
cfhttpparam( type="header", name="Content-Type", value="application/json" )
cfhttpparam( type="body", value=serializeJSON( {
severity : severityToString( logEvent.getSeverity() ),
message : logEvent.getMessage()
} ) )
}
}
function onUnRegistration() {}
}| Method | Description |
|---|---|
getProperty( name [, default] ) |
Read a config property |
setProperty( name, value ) |
Write a config property |
propertyExists( name ) |
Check if a property is set |
getProperties() |
Get all properties |
severityToString( numeric ) |
Convert severity int to human-readable |
getName() |
Appender name |
hasCustomLayout() |
True if a layout is configured |
getCustomLayout() |
Get the layout object |
isInitialized() |
True if appender is ready |
Register a custom appender in config:
appenders = {
webhook : {
class : "models.logging.WebhookAppender",
levelMin : "ERROR",
levelMax : "FATAL",
properties : { webhookUrl : "https://hooks.example.com/alerts" }
}
}
Extend AbstractLayout (standalone: logbox.system.logging.Layout, ColdBox: coldbox.system.logging.AbstractLayout). Implement one method: format( logEvent ).
// BoxLang — models/logging/JsonLayout.bx
class JsonLayout extends coldbox.system.logging.AbstractLayout {
function format( required logEvent ) {
return serializeJSON( {
"@timestamp" : dateTimeFormat( logEvent.getTimestamp(), "yyyy-mm-dd'T'HH:nn:ssZ" ),
level : logEvent.getSeverity(),
logger : logEvent.getCategory(),
message : logEvent.getMessage(),
extra : logEvent.getExtraInfo()
} )
}
}
// CFML
component extends="coldbox.system.logging.AbstractLayout" {
function format( required logEvent ) {
return serializeJSON( {
level : logEvent.getSeverity(),
logger : logEvent.getCategory(),
message : logEvent.getMessage(),
extra : logEvent.getExtraInfo()
} )
}
}Use a layout in appender config:
console : {
class : "coldbox.system.logging.appenders.ConsoleAppender",
layout : "models.logging.JsonLayout"
}
| Method | Returns |
|---|---|
getMessage() |
The log message string |
getExtraInfo() |
The extra info value (already serialized by appender) |
getSeverity() |
Numeric severity level |
getCategory() |
Category string |
getTimestamp() |
datetime of the log entry |
getAppenderName() |
Name of the calling appender |
- Use
logbox:logger:{this}— category automatically matches the class path, enabling inheritance control without code changes - Guard expensive messages — always use
canDebug()/ closure messages before building data solely for logging - Structure extraInfo as a struct — enables JSON appenders and log aggregators (ELK, Splunk, etc.) to parse fields
- Never log secrets — redact passwords, tokens, PII, and card numbers before passing them to any logger
- Development: root → DEBUG–FATAL → ConsoleAppender only (no file I/O overhead)
- Production: root → WARN–FATAL → RollingFileAppender; optionally FATAL → EmailAppender
- Category granularity: define a category per package (
models,handlers,services) — not per class — for bulk toggling in config - Use
offarray to silence noisy packages (e.g., third-party modules) without removing them from appender config - Rolling file in production: set
fileMaxSize : 5000(5 MB) andfileMaxArchives : 5to cap disk usage - Async appenders (
async : true) for high-throughput systems — be aware that async logging may lose entries on abrupt shutdown - Custom appenders for third-party sinks (Slack, PagerDuty, CloudWatch): keep all transport logic inside the appender, never inline in services