clails Logging provides a flexible logging system. It manages log output across your entire application in a unified way, supporting automatic logger selection based on package hierarchy, simultaneous output to multiple destinations, and customizable formatting.
- Logger: Receives log messages and determines whether to output them based on the configured log level
- Appender: Defines log output destinations (console, file, etc.)
- Formatter: Defines the output format for log messages (text, JSON)
- Log Level: Represents the importance of a log message (TRACE < DEBUG < INFO < WARN < ERROR < FATAL)
- Logger Hierarchy: Automatically selects loggers based on package hierarchy
clails supports the following log levels:
| Level | Description | Use Case |
|---|---|---|
:trace |
Most detailed information | Detailed trace information for debugging |
:debug |
Debug information | Debug information during development |
:info |
General information | Recording normal application operations |
:warn |
Warning | Warnings about potential issues |
:error |
Error | An error occurred but processing can continue |
:fatal |
Fatal error | Critical errors that prevent application execution |
:none |
No log output | Suppress all logs |
Use the register-logger function to register a logger.
(use-package :clails/logger)
;; Register a logger that outputs to console
(register-logger :root
:level :info
:appender (make-console-appender
:formatter (make-instance '<text-formatter>)))
;; Register a logger that outputs to file
(register-logger :sql
:level :debug
:appender (make-file-appender
:filepath "/var/log/myapp/sql.log"
:formatter (make-instance '<json-formatter>)))clails automatically selects loggers based on package hierarchy.
;; Root logger (default for all logs)
(register-logger :root
:level :info
:appender (make-console-appender
:formatter (make-instance '<text-formatter>)))
;; Logger for CLAILS package
(register-logger :clails
:level :debug
:appender (make-console-appender
:formatter (make-instance '<text-formatter>)))
;; Logger for CLAILS/MODEL package
(register-logger :clails/model
:level :trace
:appender (make-file-appender
:filepath "/var/log/myapp/model.log"
:formatter (make-instance '<json-formatter>)))If a logger is not found, it automatically searches for parent loggers:
:clails/model/query→:clails/model→:clails→:root
Use the get-logger function to retrieve a logger.
;; Get logger by name
(get-logger :root) ; => Root logger
(get-logger :clails) ; => CLAILS logger
;; Get logger from package name (automatically searches hierarchy)
(get-logger :clails/model/query) ; => :clails/model or :clails or :rootUse the remove-logger function to remove a logger.
(remove-logger :sql)Use the clear-loggers function to remove all loggers.
(clear-loggers)Outputs logs to standard output.
(make-console-appender
:formatter (make-instance '<text-formatter>))Features:
- Thread-safe: Uses the dynamic value of
*standard-output* - Safe for concurrent log output from multiple threads
Outputs logs to a file.
(make-file-appender
:filepath "/var/log/myapp/app.log"
:formatter (make-instance '<text-formatter>))Features:
- Automatically creates the file if it doesn't exist
- Appends to existing files
- File stream is automatically managed
When using file appenders, it's recommended to explicitly close them when the application terminates.
(defvar *file-appender* (make-file-appender
:filepath "/var/log/myapp/app.log"
:formatter (make-instance '<text-formatter>)))
;; On application shutdown
(close-appender *file-appender*)Outputs logs in a human-readable text format.
(make-instance '<text-formatter>)Output Example:
[2024-01-15T10:30:45.123456+09:00] INFO: User logged in user-id=123 ip-address="192.168.1.1"
[2024-01-15T10:30:46.234567+09:00] ERROR: Database connection failed error="Connection timeout"
Outputs logs in JSON format. Useful for machine processing and integration with external systems.
(make-instance '<json-formatter>)Output Example:
{"timestamp":"2024-01-15T10:30:45.123456+09:00","level":"info","message":"User logged in","user-id":123,"ip-address":"192.168.1.1"}
{"timestamp":"2024-01-15T10:30:46.234567+09:00","level":"error","message":"Database connection failed","error":"Connection timeout"}Automatically selects a logger based on the current package and outputs logs.
(in-package #:myapp/model)
;; Output logs at each log level
(log-package.trace "Entering function" :function "find-user" :args '(123))
(log-package.debug "Query executed" :query "SELECT * FROM users WHERE id = ?" :params '(123))
(log-package.info "User found" :user-id 123 :user-name "Alice")
(log-package.warn "Slow query detected" :query-time 5.2 :threshold 3.0)
(log-package.error "Failed to save user" :error "Validation failed" :user-id 123)
(log-package.fatal "Database connection lost" :error "Connection timeout")Logger Selection:
- When the current package is
#:myapp/model - Searches for loggers in order:
:myapp/model→:myapp→:root - Uses the first logger found
Explicitly specify a logger name to output logs.
(log-to :root :info "Application started")
(log-to :sql :debug "Query executed" :query "SELECT * FROM users")
(log-to :myapp/service :error "Service failed" :service "UserService")Use specialized logging macros for specific purposes.
Used for recording database queries.
(log.sql "SELECT * FROM users WHERE id = ?"
:params '(123)
:query-time 0.05
:rows-affected 1)- Logger name:
:sql - Log level:
:debug
Used for recording HTTP requests/responses.
(log.web-access "GET /users/123"
:method "GET"
:path "/users/123"
:status 200
:duration 0.123
:ip-address "192.168.1.1")- Logger name:
:web-access - Log level:
:info
Used for recording security-related events and user actions.
(log.audit "User login successful"
:user-id 123
:username "alice"
:ip-address "192.168.1.1"
:action "login")- Logger name:
:audit - Log level:
:info
Used for recording task execution events.
(log.task "Task started"
:task-name "db:migrate"
:namespace :db)
(log.task "Task completed"
:task-name "cleanup"
:namespace :system
:duration 1.234
:status :success)- Logger name:
:task - Log level:
:info
Use the with-log-context macro to add common context to all log messages within a block.
(with-log-context (:request-id "req-12345" :user-id 123)
(log-package.info "Processing request")
(log-package.debug "Fetching user data")
(log-package.info "Request completed"))
;; Output example:
;; [2024-01-15T10:30:45.123456+09:00] INFO: Processing request request-id="req-12345" user-id=123
;; [2024-01-15T10:30:45.234567+09:00] DEBUG: Fetching user data request-id="req-12345" user-id=123
;; [2024-01-15T10:30:45.345678+09:00] INFO: Request completed request-id="req-12345" user-id=123Contexts can be nested.
(with-log-context (:request-id "req-12345")
(log-package.info "Request started")
(with-log-context (:operation "fetch-user" :user-id 123)
(log-package.debug "Fetching user")
(log-package.info "User fetched"))
(log-package.info "Request completed"))
;; Output example:
;; [2024-01-15T10:30:45.123456+09:00] INFO: Request started request-id="req-12345"
;; [2024-01-15T10:30:45.234567+09:00] DEBUG: Fetching user request-id="req-12345" operation="fetch-user" user-id=123
;; [2024-01-15T10:30:45.345678+09:00] INFO: User fetched request-id="req-12345" operation="fetch-user" user-id=123
;; [2024-01-15T10:30:45.456789+09:00] INFO: Request completed request-id="req-12345"Log levels can be changed dynamically during application execution.
;; Change the log level of a logger
(set-logger-level :root :debug)
(set-logger-level :clails/model :trace)
(set-logger-level :sql :info);; Check if the specified log level is enabled for the current package
(log-level-enabled-p :debug) ; => T or NIL
;; Check if the specified log level is enabled for a specific package
(log-level-enabled-p :debug :clails/model) ; => T or NIL
(log-level-enabled-p :trace :sql) ; => T or NILExecute expensive operations and log only when the log level is enabled.
(when (log-level-enabled-p :debug)
(let ((expensive-data (compute-expensive-debug-info)))
(log-package.debug "Debug info" :data expensive-data)))Appenders can be added to loggers at runtime.
;; Add an appender to an existing logger
(add-appender :root
(make-file-appender
:filepath "/var/log/myapp/debug.log"
:formatter (make-instance '<text-formatter>)))
;; Logger with multiple appenders
(let ((logger (get-logger :root)))
;; This logger outputs to both console and file
(log-to :root :info "This message goes to both console and file"))(defun setup-logging ()
"Initialize application logging configuration"
;; Root logger: Console and general log file
(register-logger :root
:level :info
:appender (make-console-appender
:formatter (make-instance '<text-formatter>)))
(add-appender :root
(make-file-appender
:filepath "/var/log/myapp/app.log"
:formatter (make-instance '<text-formatter>)))
;; SQL logger: SQL-specific log file (JSON format)
(register-logger :sql
:level :debug
:appender (make-file-appender
:filepath "/var/log/myapp/sql.log"
:formatter (make-instance '<json-formatter>)))
;; Web access log: Access log file
(register-logger :web-access
:level :info
:appender (make-file-appender
:filepath "/var/log/myapp/access.log"
:formatter (make-instance '<text-formatter>)))
;; Audit log: Audit-specific file (JSON format)
(register-logger :audit
:level :info
:appender (make-file-appender
:filepath "/var/log/myapp/audit.log"
:formatter (make-instance '<json-formatter>)))
;; Task log: Task execution log file
(register-logger :task
:level :info
:appender (make-file-appender
:filepath "/var/log/myapp/task.log"
:formatter (make-instance '<text-formatter>))))
;; On application startup
(setup-logging)(in-package #:myapp/controller)
(defun show-user (params)
"Display user information"
(let ((user-id (parse-integer (getf params :id))))
(with-log-context (:action "show-user" :user-id user-id)
(log-package.info "Fetching user")
(handler-case
(let ((user (find-user-by-id user-id)))
(if user
(progn
(log-package.info "User found")
(render-user user))
(progn
(log-package.warn "User not found")
(render-404))))
(error (e)
(log-package.error "Failed to fetch user" :error (princ-to-string e))
(render-500))))))(in-package #:myapp/model)
(defmethod save ((user <user>))
"Save user"
(log-package.debug "Saving user" :user-id (ref user :id))
(when (log-level-enabled-p :trace)
(log-package.trace "User data" :data (show-model-data user)))
(handler-case
(progn
(validate user)
(if (has-error-p user)
(progn
(log-package.warn "Validation failed"
:errors (get-all-errors user))
nil)
(progn
(with-transaction
(if (ref user :id)
(update1 user)
(insert1 user)))
(log-package.info "User saved successfully"
:user-id (ref user :id))
user)))
(error (e)
(log-package.error "Failed to save user"
:error (princ-to-string e)
:user-id (ref user :id))
(error e))))(defun setup-logging-for-environment ()
"Configure logging based on environment"
(ecase (get-environment)
(:development
;; Development: All at DEBUG level, console output
(register-logger :root
:level :debug
:appender (make-console-appender
:formatter (make-instance '<text-formatter>))))
(:test
;; Test: All at INFO level, file output
(register-logger :root
:level :info
:appender (make-file-appender
:filepath "/tmp/test.log"
:formatter (make-instance '<text-formatter>))))
(:production
;; Production: INFO level, JSON format file output
(register-logger :root
:level :info
:appender (make-file-appender
:filepath "/var/log/myapp/app.log"
:formatter (make-instance '<json-formatter>)))
;; Error logs go to a separate file
(register-logger :root
:level :error
:appender (make-file-appender
:filepath "/var/log/myapp/error.log"
:formatter (make-instance '<json-formatter>))))));; Recommended: Package-based logging
(log-package.info "User created" :user-id 123)
;; Not recommended: Explicitly specifying logger name (unless necessary)
(log-to :myapp/controller :info "User created" :user-id 123);; TRACE: Detailed debug information (usually disabled)
(log-package.trace "Function arguments" :args args)
;; DEBUG: Debug information during development
(log-package.debug "Query result" :count (length results))
;; INFO: Normal operation recording
(log-package.info "User logged in" :user-id 123)
;; WARN: Potential issues
(log-package.warn "Slow query detected" :query-time 5.2)
;; ERROR: Error occurred but can continue
(log-package.error "Failed to send email" :error error-message)
;; FATAL: Critical error
(log-package.fatal "Database connection lost" :error error-message);; Recommended: Structured with keyword arguments
(log-package.info "User action"
:user-id 123
:action "update"
:resource "profile")
;; Not recommended: Embedding information in message
(log-package.info (format nil "User ~A performed ~A on ~A" 123 "update" "profile"));; Recommended: Log level check
(when (log-level-enabled-p :debug)
(let ((debug-info (expensive-debug-computation)))
(log-package.debug "Debug information" :info debug-info)))
;; Not recommended: Always execute expensive operation
(log-package.debug "Debug information"
:info (expensive-debug-computation));; Recommended: Share request ID with with-log-context
(with-log-context (:request-id request-id)
(process-request)
(save-result)
(send-response))
;; Not recommended: Manually add to all logs
(log-package.info "Processing" :request-id request-id)
(log-package.info "Saved" :request-id request-id)
(log-package.info "Sent" :request-id request-id)Cause 1: Logger not registered
;; Check: Is the logger registered?
(get-logger :root) ; => NIL means not registered
;; Solution: Register the logger
(register-logger :root
:level :info
:appender (make-console-appender
:formatter (make-instance '<text-formatter>)))Cause 2: Log level not appropriate
;; Check: Is the log level enabled?
(log-level-enabled-p :debug) ; => NIL means disabled
;; Solution: Change log level
(set-logger-level :root :debug)Cause 3: Hierarchy mismatch
;; Logging from :myapp/controller package
(in-package #:myapp/controller)
(log-package.info "Test") ; => Not output
;; Solution: Register root logger
(register-logger :root
:level :info
:appender (make-console-appender
:formatter (make-instance '<text-formatter>)))
;; Or: Register logger for the package
(register-logger :myapp
:level :info
:appender (make-console-appender
:formatter (make-instance '<text-formatter>)))Cause: Invalid file path
;; Solution: Ensure directory exists
(ensure-directories-exist "/var/log/myapp/")
(register-logger :root
:level :info
:appender (make-file-appender
:filepath "/var/log/myapp/app.log"
:formatter (make-instance '<text-formatter>)))The clails Logging system has the following features:
- Hierarchical Loggers: Automatically selects loggers based on package hierarchy
- Flexible Output Destinations: Supports multiple destinations such as console and file
- Customizable Formatting: Text, JSON, and other formats suitable for different purposes
- Dynamic Configuration: Log levels and appenders can be changed at runtime
- Context Management: Structured log output with
with-log-context - Purpose-Specific Logging: Logging macros for SQL, web access, audit, tasks, etc.
- Thread-Safe: Safe log output from multiple threads
For detailed API reference, please refer to the docstring of each function.