Using logger.factory for module logsfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Imagine your module breaks in production at 3 AM, and the only clue is a user's vague complaint that "the form didn't work." Without logging, you're debugging blind. Drupal's logging system is the flight recorder for your code — every important event, error, or state change gets written down somewhere a developer can find it later, long after the moment has passed.

What you'll learn in this lesson

  • How to get a logger instance for your own module via the logger.factory service
  • The full range of RFC 5424 severity levels Drupal's logger supports, and when to use each
  • How to inject the logger service properly instead of calling \Drupal::logger() directly
  • Where those log messages actually end up, and how to find them

Getting a logger for your module

Every logger in Drupal belongs to a named channel — a string that tags every message so you can filter by source later. The simplest way to get one:

\Drupal::logger('my_module')->notice('User %uid completed checkout.', ['%uid' => $account->id()]);

\Drupal::logger('my_module') is a static shortcut into the logger.factory service, asking it for a channel named my_module. That channel name is exactly what shows up in the Type column when you go looking for these messages later — pick your module's machine name so there's never any ambiguity about where a log entry came from.

The severity levels, and when each one is right

  • emergency() — the system is unusable. Reserve this for genuinely catastrophic failures.
  • alert() — action must be taken immediately (a security breach, a full disk).
  • critical() — critical conditions (a required service is down).
  • error() — a runtime error that doesn't require immediate action but should be tracked and fixed.
  • warning() — an exceptional occurrence that isn't an error (a deprecated API call, a fallback path taken).
  • notice() — normal but significant events (the checkout example above).
  • info() — routine, informational messages.
  • debug() — detailed debug information, usually silenced in production.
Placeholders, not string concatenation: notice the %uid token above rather than "User " . $account->id(). Drupal's logger runs your message through its own placeholder substitution (the same %/@/: convention as t()), which keeps dynamic values out of the raw message string — useful for grouping identical log entries together in reports, and safer if the value ever contains user input.

Injecting the logger properly

The static \Drupal::logger() call above is fine for a quick script, but inside a service, controller, or form class, inject logger.factory like any other dependency:

use Psr\Log\LoggerInterface;

class MyService {

  public function __construct(
    protected LoggerInterface $logger,
  ) {}

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('logger.factory')->get('my_module'),
    );
  }

  public function processOrder(int $orderId): void {
    $this->logger->notice('Processing order #@id.', ['@id' => $orderId]);
  }

}

Notice the injected type is Psr\Log\LoggerInterface, not some Drupal-specific logger class — Drupal's logging system implements the standard PSR-3 interface, so any code you write against it is portable to other PHP projects that follow the same standard.

See it for yourself

Log a test message from any custom code (even a quick \Drupal::logger('my_module')->notice('test') in a controller), then visit Reports → Recent log messages (/admin/reports/dblog) on your Drupal site. Filter the Type column to your module's machine name and you'll see every message you've logged, each with a timestamp, severity icon, and the exact text.

Quick check: you're logging a message every time a user submits a contact form — is that notice() or info()? Either could arguably work, but notice() is the better fit: it's a normal, expected event that's still worth tracking (someone submitted the form), whereas info() is better reserved for lower-value routine chatter you'd rarely need to search for specifically.

Key takeaways

  • \Drupal::logger('channel') (or the injected logger.factory service) is how every log message gets written in Drupal — always give it your module's machine name as the channel.
  • Use the standard 8 RFC 5424 severity levels correctly — they control how a message is filtered, surfaced, and prioritized by anyone reading the log later.
  • Pass dynamic values as placeholders (%value, @value, :value) rather than concatenating strings, exactly like t().
  • Inject logger.factory as a Psr\Log\LoggerInterface in services/controllers rather than calling the static \Drupal::logger() helper, for testability.
  • Every logged message lands in /admin/reports/dblog by default, filterable by channel and severity.

Coming up next

One channel is fine for a small module, but larger ones often want to separate concerns — a "my_module.security" channel distinct from "my_module.performance," say. Next, we'll look at defining a custom log channel of your own.