A bug that only happens in production, at 2am, with no stack trace, is every developer's least favorite kind. Good debug logging is how you turn "it broke somehow" into "here's exactly what value was wrong and where" — without needing to reproduce the failure live in front of you. Drupal's logging system is built for exactly this: structured, filterable, and safe to leave enabled in production without leaking sensitive data if you use it correctly.
What you'll learn in this lesson
- How to get a logger scoped to your own module and write messages through it
- The severity levels available, and which one to reach for in which situation
- Where those messages actually end up, and how to filter them by channel
- Why
\Drupal::logger()is safe to call even in code paths that might run before full bootstrap
Getting and using a logger
// Inside a class with dependency injection available:
public function __construct(LoggerChannelFactoryInterface $logger_factory) {
$this->logger = $logger_factory->get('my_module');
}
public function processOrder(array $order) {
$this->logger->debug('Processing order @id with @count items.', [
'@id' => $order['id'],
'@count' => count($order['items']),
]);
if (empty($order['items'])) {
$this->logger->warning('Order @id has no items — skipping.', ['@id' => $order['id']]);
return;
}
}
// In procedural code, or when injection isn't set up yet:
\Drupal::logger('my_module')->error('Order processing failed: @message', ['@message' => $e->getMessage()]);
The string passed to get() or logger() — 'my_module' here — is the channel: a label that groups every log entry from your module together, filterable independently of every other module's log noise. Notice the message uses the same @placeholder pattern as t() — it's not for translation here, it's for safe, consistent value interpolation with the same escaping guarantees.
Severity levels, from quietest to loudest
debug()— fine-grained detail useful only while actively developing; noisy, usually disabled in production log views.info()— routine events worth a record but not a concern: "cache cleared," "cron completed."notice()— normal but noteworthy — a one-time confirmation message, a config value falling back to a default.warning()— something recoverable went sideways: a missing optional value, a deprecated code path in use.error()— an operation failed outright but the site keeps running.critical()/alert()/emergency()— reserved for site-wide failures; you'll rarely call these from ordinary module code.
Choosing the right level matters beyond convention — most log-viewing tools, including Drupal's own dblog, let an administrator filter by minimum severity, so a debug() call buried at error() severity means real problems get lost in noise, and vice versa.
$order array, a password, or an API key "just in case" is a real, common security mistake — dblog entries are visible to any user with the "access site reports" permission, and file-based logs often end up in less carefully guarded places than your database. Log identifiers (an order ID, a user ID) and interpolate only the specific values you actually need to diagnose the problem.Quick check: a scheduled import job fails to parse three rows out of ten thousand but completes successfully otherwise. Which severity fits best —
error()orwarning()? (Reasonable case for either, butwarning()is usually the better fit: the overall job succeeded, so this isn't a failure of the operation itself, just a partial, recoverable issue worth an administrator's attention.)
Key takeaways
- Get a channel-scoped logger via
\Drupal::logger('your_module')or injectlogger.factoryand call->get('your_module'). - Use the same
@variable/%variableplaceholder syntax ast()for safe interpolation in log messages. - Pick a severity that matches real operational meaning — it directly affects whether an administrator sees the message when filtering logs.
- Never log secrets, full user-submitted payloads, or credentials — log identifiers and the specific diagnostic values you need instead.
Coming up next
Logging tells you what happened after the fact. Next, we look at the Devel module — the standard tool for inspecting what's happening live, right now, while you're actively building and debugging a module.