From coldbox-agent-skills
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/coldbox-agent-skills:logboxThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Load this skill when:
Load this skill when:
extraInfo logging or implementing $toString()canDebug() / closure messagesExamples 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:
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 logbox
Add 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 with configure() methodconfig/ColdBox.cfc logbox variable — inline ColdBox configLogBox constructor (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.
config/LogBox.cfc// 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" }
}
}
}
config/ColdBox.cfc)// 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 : "[email protected]",
to : "[email protected]",
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 )#" )
can{} guardif( 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.
$toString()extraInfo accepts any value. Appenders serialize it using this algorithm:
$toString() → call it and use the return string$toString() → serialize to XML$toString() on domain objectsclass 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 |
logbox:logger:{this} — category automatically matches the class path, enabling inheritance control without code changescanDebug() / closure messages before building data solely for loggingmodels, handlers, services) — not per class — for bulk toggling in configoff array to silence noisy packages (e.g., third-party modules) without removing them from appender configfileMaxSize : 5000 (5 MB) and fileMaxArchives : 5 to cap disk usageasync : true) for high-throughput systems — be aware that async logging may lose entries on abrupt shutdownnpx claudepluginhub coldbox/skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.