Skip to content

Instantly share code, notes, and snippets.

@aklump
Last active May 7, 2026 14:22
Show Gist options
  • Select an option

  • Save aklump/a90467ce56d3f555c2af6f8c9a11e159 to your computer and use it in GitHub Desktop.

Select an option

Save aklump/a90467ce56d3f555c2af6f8c9a11e159 to your computer and use it in GitHub Desktop.
Provides class-scoped logging helpers for Drupal custom classes.
<?php
use Drupal\Core\Logger\LoggerChannelInterface;
use Psr\Log\LogLevel;
/**
* Provides class-scoped logging helpers.
*
* Classes using this trait get a logger channel derived from the consuming
* class name. The channel uses the short class name so Drupal's log UI remains
* easy to scan and filter. Long class names are shortened and given a stable
* hash suffix to stay within watchdog channel length limits while reducing
* collision risk.
*
* The logger channel name is public so callers can use the same class-derived
* channel with Drupal's logging APIs directly when they need behavior not
* provided by this trait.
*
* The exception helper is intentionally lightweight and PSR-3 oriented. It is
* intended for quick, severity-aware exception logging during normal
* application flow. Use \Drupal\Core\Utility\Error::logException() directly
* when Drupal's full exception logging format is required.
*/
trait ClassLoggerTrait {
/**
* Gets the logger channel name for the calling class.
*
* The short class name is used because Drupal's log UI exposes the channel as
* a primary scanning/filtering column. Long names are shortened with a hash
* suffix to avoid watchdog channel length issues while reducing collision
* risk.
*/
public static function loggerChannelName(): string {
static $channels = [];
$class = static::class;
if (!isset($channels[$class])) {
$namespace_position = strrpos($class, '\\');
$short_name = $namespace_position === FALSE
? $class
: substr($class, $namespace_position + 1);
if (strlen($short_name) > 32) {
$short_name = substr($short_name, 0, 25) . '_' . substr(hash('crc32b', $class), 0, 6);
}
$channels[$class] = $short_name;
}
return $channels[$class];
}
/**
* Gets the class logger.
*
* Intentionally do not statically cache the logger factory. Drupal's
* container already returns shared service instances, and keeping our own
* copy can make tests/container rebuilds harder to reason about for
* negligible gain.
*/
protected static function logger(): LoggerChannelInterface {
return \Drupal::logger(static::loggerChannelName());
}
/**
* Logs an exception using the class-derived logger channel.
*
* @param \Throwable $exception
* The exception or error to log.
* @param string $level
* A PSR-3 log level from \Psr\Log\LogLevel.
* @param array $context
* Additional context variables for the log message.
*/
protected static function logException(\Throwable $exception, string $level = LogLevel::ERROR, array $context = []): void {
if ($exception instanceof \Exception) {
watchdog_exception(static::loggerChannelName(), $exception, NULL, $context, $level);
return;
}
// Drupal 9's watchdog_exception() only accepts \Exception. Log other
// \Throwable instances directly until this can use Error::logException().
$context += [
'exception' => $exception,
'@type' => get_class($exception),
'@message' => $exception->getMessage(),
'@file' => $exception->getFile(),
'@line' => $exception->getLine(),
];
static::logger()->log($level, '@type: @message in @file on line @line', $context);
// TODO Drupal 10+: replace this method body with:
// \Drupal\Core\Utility\Error::logException(static::logger(), $exception, NULL, $context, $level);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment