diff --git a/includes/mollom.class.inc b/includes/mollom.class.inc
index 1ffab1a..616e699 100644
--- a/includes/mollom.class.inc
+++ b/includes/mollom.class.inc
@@ -439,39 +439,32 @@ abstract class Mollom {
       }
     }
 
+    $request_info = array(
+      'request' => $method . ' ' . $server . '/' . $path,
+      'headers' => $headers,
+      'data' => $data,
+      'response_code' => $response->code,
+      'response_message' => $response->message,
+      'response' => !empty($response->data) ? $response->data : $response->body,
+    );
     if ($response->isError) {
-      $arguments = array(
-        'request' => $method . ' ' . $server . '/' . $path,
-        'headers' => $headers,
-        'data' => $data,
-        'response' => !empty($response->data) ? $response->data : $response->body,
-      );
       if ($response->code <= 0) {
-        throw new MollomNetworkException('Network error.', self::NETWORK_ERROR, NULL, $this, $arguments);
+        throw new MollomNetworkException('Network error.', self::NETWORK_ERROR, NULL, $this, $request_info);
       }
       if ($response->code == self::AUTH_ERROR) {
-        throw new MollomAuthenticationException('Invalid authentication.', self::AUTH_ERROR, NULL, $this, $arguments);
+        throw new MollomAuthenticationException('Invalid authentication.', self::AUTH_ERROR, NULL, $this, $request_info);
       }
       if ($response->code == self::RESPONSE_ERROR || $response->code >= 500) {
-        throw new MollomResponseException($response->message, $response->code, NULL, $this, $arguments);
+        throw new MollomResponseException($response->message, $response->code, NULL, $this, $request_info);
       }
-      throw new MollomException($response->message, $response->code, NULL, $this, $arguments);
+      throw new MollomException($response->message, $response->code, NULL, $this, $request_info);
     }
     else {
       $this->lastResponseCode = TRUE;
+      // No message is logged in case of success.
       $this->log[] = array(
         'severity' => 'debug',
-        'message' => '@code @method @uri',
-        'arguments' => array(
-          '@code' => $response->code,
-          '@method' => $method,
-          '@uri' => $path,
-        ),
-        'request' => $method . ' ' . $server . '/' . $path,
-        'headers' => $headers,
-        'data' => $data,
-        'response' => $response->data,
-      );
+      ) + $request_info;
 
       return $response->data;
     }
@@ -1258,14 +1251,17 @@ class MollomException extends Exception {
   /**
    * The severity of this exception.
    *
+   * By default, all exceptions should be logged and appear as errors (unless
+   * overridden by a later log entry).
+   *
    * @var string
    */
-  protected $severity = 'debug';
+  protected $severity = 'error';
 
   /**
    * Overrides Exception::__construct().
    */
-  function __construct($message = '', $code = 0, Exception $previous = NULL, Mollom $mollom, array $arguments = array()) {
+  function __construct($message = '', $code = 0, Exception $previous = NULL, Mollom $mollom, array $request_info = array()) {
     // Fatal error on PHP <5.3 when passing more arguments to Exception.
     if (version_compare(phpversion(), '5.3') >= 0) {
       parent::__construct($message, $code, $previous);
@@ -1288,8 +1284,8 @@ class MollomException extends Exception {
       ),
     );
     // Add HTTP request information, if available.
-    if (!empty($arguments)) {
-      $message += $arguments;
+    if (!empty($request_info)) {
+      $message += $request_info;
     }
     $mollom->log[] = $message;
   }
@@ -1302,7 +1298,12 @@ class MollomException extends Exception {
  * communication error.
  */
 class MollomNetworkException extends MollomException {
-  protected $severity = 'error';
+  /**
+   * Overrides MollomException::$severity.
+   *
+   * The client may be able to recover from this error, so use a warning level.
+   */
+  protected $severity = 'warning';
 }
 
 /**
@@ -1311,7 +1312,6 @@ class MollomNetworkException extends MollomException {
  * Thrown in case API keys or other authentication parameters are invalid.
  */
 class MollomAuthenticationException extends MollomException {
-  protected $severity = 'error';
 }
 
 /**
@@ -1324,6 +1324,12 @@ class MollomAuthenticationException extends MollomException {
  * @see Mollom::handleRequest()
  */
 class MollomResponseException extends MollomException {
+  /**
+   * Overrides MollomException::$severity.
+   *
+   * Might be a client-side error, but more likely a server-side error. The
+   * client may be able to recover from this error.
+   */
   protected $severity = 'debug';
 }
 
diff --git a/mollom.admin.inc b/mollom.admin.inc
index c9e76f9..fe20b3a 100644
--- a/mollom.admin.inc
+++ b/mollom.admin.inc
@@ -677,7 +677,7 @@ function mollom_admin_blacklist_delete_submit($form, &$form_state) {
   else {
     drupal_set_message(t('An error occurred upon trying to remove the item from the blacklist.'), 'error');
     _mollom_watchdog(array(
-      'Failed to removed @value (%context) from @reason blacklist.' => $args,
+      'Failed to remove @value (%context) from @reason blacklist.' => $args,
       'Data:<pre>@data</pre>' => array('@data' => $form_state['values']['entry']),
       'Result:<pre>@result</pre>' => array('@result' => $result),
     ), WATCHDOG_ERROR);
diff --git a/mollom.drupal.inc b/mollom.drupal.inc
index 1cc5b71..100fa94 100644
--- a/mollom.drupal.inc
+++ b/mollom.drupal.inc
@@ -80,48 +80,27 @@ class MollomDrupal extends Mollom {
    * Overrides Mollom::writeLog().
    */
   function writeLog() {
-    $messages = array();
-    foreach ($this->log as $i => $entry) {
-      $entry += array('arguments' => array());
-      $message = array(
-        $entry['message'] => $entry['arguments'],
-      );
-      if (isset($entry['request'])) {
-        $message['Request: @request<pre>@parameters</pre>'] = array(
-          '@request' => $entry['request'],
-          '@parameters' => !empty($entry['data']) ? $entry['data'] : '',
-        );
-      }
-      if (isset($entry['headers'])) {
-        $message['Request headers:<pre>@headers</pre>'] = array(
-          '@headers' => $entry['headers'],
-        );
+    foreach ($this->log as $entry) {
+      // @todo Do these always exist?
+      if (1 || isset($entry['request'])) {
+        $entry['Request: ' . $entry['request']] = !empty($entry['data']) ? $entry['data'] : NULL;
+        unset($entry['request'], $entry['data']);
       }
-      if (isset($entry['response'])) {
-        $message['Response:<pre>@response</pre>'] = array(
-          '@response' => $entry['response'],
-        );
+      if (1 || isset($entry['headers'])) {
+        $entry['Request headers:'] = $entry['headers'];
+        unset($entry['headers']);
       }
-      $messages[] = $message;
-
-      // Translate log messages for debugging without watchdog.
-      // @todo Move into mollom.unit.inc implementation.
-      /*
-      $output = array();
-      foreach ($message as $text => $args) {
-        foreach ($args as &$arg) {
-          if (is_array($arg)) {
-            $arg = var_export($arg, TRUE);
-          }
-        }
-        $output[] = strtr($text, $args);
+      if (1 || isset($entry['response'])) {
+        $entry['Response: ' . $entry['response_code'] . ' ' . $entry['response_message']] = $entry['response'];
+        unset($entry['response'], $entry['response_code'], $entry['response_message']);
       }
-      $this->log[$i]['message'] = implode("\n", $output);
-      unset($this->log[$i]['arguments']);
-      drupal_set_message(implode('<br />', $output)); //, $this->log[$i]['severity']);
-      */
+      // The client class contains the logic for recovering from certain errors,
+      // and log messages are only written after that happened. Therefore, we
+      // can normalize the severity of all log entries to the overall success or
+      // failure of the attempted request.
+      // @see Mollom::query()
+      mollom_log($entry, $this->lastResponseCode === TRUE ? WATCHDOG_DEBUG : WATCHDOG_ERROR);
     }
-    _mollom_watchdog_multiple($messages, $this->lastResponseCode === TRUE ? WATCHDOG_DEBUG : WATCHDOG_ERROR);
 
     // After writing log messages, empty the log.
     $this->purgeLog();
diff --git a/mollom.module b/mollom.module
index 0de6074..c727066 100644
--- a/mollom.module
+++ b/mollom.module
@@ -1265,6 +1265,16 @@ function _mollom_status($reset = FALSE) {
   // If we have keys and are asked to reset, check whether keys are valid.
   if ($status['keys'] && $reset) {
     $status['keys valid'] = $mollom->verifyKeys();
+    if ($status['keys valid'] === TRUE) {
+      mollom_log(array(
+        'message' => 'API keys are valid.',
+      ), WATCHDOG_INFO);
+    }
+    else {
+      mollom_log(array(
+        'message' => 'Invalid API keys.',
+      ), WATCHDOG_ERROR);
+    }
   }
   // Otherwise, if there are no keys, they cannot be valid.
   elseif (!$status['keys']) {
@@ -1568,10 +1578,9 @@ function mollom_validate_analysis(&$form, &$form_state) {
     else {
       $form_state['mollom']['require_moderation'] = TRUE;
     }
-    _mollom_watchdog(array(
-      'Profanity: %teaser' => array('%teaser' => $teaser),
-      'Data:<pre>@data</pre>' => array('@data' => $data),
-      'Result:<pre>@result</pre>' => array('@result' => $result),
+    mollom_log(array(
+      'message' => 'Profanity: %teaser',
+      'arguments' => array('%teaser' => $teaser),
     ));
   }
 
@@ -1587,10 +1596,9 @@ function mollom_validate_analysis(&$form, &$form_state) {
     switch ($result['spamClassification']) {
       case 'ham':
         $form_state['mollom']['require_captcha'] = FALSE;
-        _mollom_watchdog(array(
-          'Ham: %teaser' => array('%teaser' => $teaser),
-          'Data:<pre>@data</pre>' => array('@data' => $data),
-          'Result:<pre>@result</pre>' => array('@result' => $result),
+        mollom_log(array(
+          'message' => 'Ham: %teaser',
+          'arguments' => array('%teaser' => $teaser),
         ), WATCHDOG_INFO);
         break;
 
@@ -1602,18 +1610,16 @@ function mollom_validate_analysis(&$form, &$form_state) {
         else {
           $form_state['mollom']['require_moderation'] = TRUE;
         }
-        _mollom_watchdog(array(
-          'Spam: %teaser' => array('%teaser' => $teaser),
-          'Data:<pre>@data</pre>' => array('@data' => $data),
-          'Result:<pre>@result</pre>' => array('@result' => $result),
+        mollom_log(array(
+          'message' => 'Spam: %teaser',
+          'arguments' => array('%teaser' => $teaser),
         ));
         break;
 
       case 'unsure':
-        _mollom_watchdog(array(
-          'Unsure: %teaser' => array('%teaser' => $teaser),
-          'Data:<pre>@data</pre>' => array('@data' => $data),
-          'Result:<pre>@result</pre>' => array('@result' => $result),
+        mollom_log(array(
+          'message' => 'Unsure: %teaser',
+          'arguments' => array('%teaser' => $teaser),
         ), WATCHDOG_INFO);
 
         // Only throw a validation error and retrieve a CAPTCHA, if we check
@@ -1633,10 +1639,9 @@ function mollom_validate_analysis(&$form, &$form_state) {
         // Normally, this should not happen, but if it does, log it. As there
         // could be multiple reasons for this, it is not safe to trigger the
         // fallback mode.
-        _mollom_watchdog(array(
-          'Unknown: %teaser' => array('%teaser' => $teaser),
-          'Data:<pre>@data</pre>' => array('@data' => $data),
-          'Result:<pre>@result</pre>' => array('@result' => $result),
+        mollom_log(array(
+          'message' => 'Unknown: %teaser',
+          'arguments' => array('%teaser' => $teaser),
         ));
         break;
     }
@@ -1844,6 +1849,234 @@ function mollom($class = NULL) {
 }
 
 /**
+ * Implements hook_exit().
+ */
+function mollom_exit() {
+  // Write log messages.
+  mollom_log_write();
+}
+
+function mollom_log(array $entry = NULL, $severity = NULL, $reset = FALSE) {
+  $log = &drupal_static(__FUNCTION__, array());
+
+  if ($reset) {
+    $return = $log;
+    $log = array();
+    return $return;
+  }
+  if (!isset($entry)) {
+    return $log;
+  }
+  // Take over the primary message.
+  
+  // @todo Last message might come from class instead of module; prefer the last module's message?
+  if (isset($entry['message'])) {
+    $log['message'] = $entry['message'];
+    $log['arguments'] = isset($entry['arguments']) ? $entry['arguments'] : array();
+    // Do not duplicate the message in the log of entries.
+    unset($entry['message'], $entry['arguments']);
+
+    // Default to notice severity for module messages.
+    if (!isset($severity)) {
+      $severity = WATCHDOG_NOTICE;
+    }
+  }
+  // (Global) severity is always updated whenever passed. We cannot only update
+  // when the new entry is more severe than the existing, because the module is
+  // able to recover from certain errors.
+  // @todo Wait - the *client* is able to recover, not the module. Should
+  //   writeLog() simply check for the final lastResponseCode === TRUE and pass
+  //   debug vs. error based on that? Yes. Comment needs to be updated.
+  if (isset($severity)) {
+    $log['severity'] = $severity;
+  }
+  $log['entries'][] = $entry;
+
+  return $log;
+}
+
+/**
+ *
+ *
+ * @see watchdog()
+ */
+function mollom_log_write() {
+  // Retrieve the log and reset it.
+  $log = mollom_log(NULL, NULL, TRUE);
+  if (empty($log)) {
+    return;
+  }
+  list($message, $arguments) = _mollom_format_log($log);
+
+  // @todo Is there always a module log entry?
+  watchdog('mollom', $message, $arguments, $log['severity']);
+}
+
+/**
+ * Log a Mollom system message.
+ *
+ * @param $log
+ *   @todo A list of message parts. Each item is an associative array whose keys are
+ *   log message strings and whose corresponding values are t()-style
+ *   replacement token arguments. At least one part is required.
+ *
+ * @see watchdog()
+ */
+function _mollom_format_log(array $log) {
+  $message = isset($log['message']) ? $log['message'] : '';
+  $arguments = isset($log['arguments']) ? $log['arguments'] : array();
+
+  // Hide further message details in the log overview table, if any.
+  // @see theme_dblog_message()
+  if (!empty($log['entries'])) {
+    #$message = str_pad($message, 56, ' ', STR_PAD_RIGHT);
+    // A <br> would be more appropriate, but filter_xss_admin() does not allow it.
+    #$message = '<p>' . $message . '</p>' . "\n\n";
+    $message = $message . "<p>\n\n</p>";
+  }
+
+  // Walk through each log entry to prepare and format its message and arguments.
+  $i = 0;
+  foreach ($log['entries'] as $entry) {
+    // Take over message and arguments literally (if any).
+    if (isset($entry['message'])) {
+      if (!empty($entry['arguments'])) {
+        $message .= _mollom_format_string($entry['message'], $entry['arguments']);
+        unset($entry['arguments']);
+      }
+      else {
+        $message .= $entry['message'];
+      }
+      unset($entry['message']);
+      $message .= "\n";
+    }
+    unset($entry['severity']);
+
+    // Prettify replacement token values, if possible.
+    foreach ($entry as $token => $array) {
+      // Only prettify non-scalar values plus Booleans.
+      // I.e., NULL, TRUE, FALSE, array, and object.
+      if (is_scalar($array) && !is_bool($array)) {
+        continue;
+      }
+      $flat_value = NULL;
+      // Convert arrays and objects.
+      // @todo Objects?
+      if (isset($array) && !is_scalar($array)) {
+        $ref = &$array;
+        $key = key($ref);
+        $parents = array();
+        $flat_value = '';
+        while ($key !== NULL) {
+          if (is_scalar($ref[$key]) || is_bool($ref[$key]) || is_null($ref[$key])) {
+            $value = var_export($ref[$key], TRUE);
+            // Indent all values to have a visual separation from the last.
+            $flat_value .= str_repeat('  ', count($parents) + 1) . "{$key} = {$value}\n";
+          }
+
+          // Recurse into nested keys, if the current key is not scalar.
+          if (is_array($ref[$key])) {
+            $flat_value .= str_repeat('  ', count($parents) + 1) . "{$key} =\n";
+            $parents[] = &$ref;
+            $ref = &$ref[$key];
+            $key = key($ref);
+          }
+          else {
+            // Move to next key if there is one.
+            next($ref);
+            if (key($ref) !== NULL) {
+              $key = key($ref);
+            }
+            // Move back to parent key, if there is one.
+            elseif ($parent = array_pop($parents)) {
+              $ref = &$parent;
+              next($ref);
+              $key = key($ref);
+            }
+            // Otherwise, reached the end of array and recursion.
+            else {
+              $key = NULL;
+            }
+          }
+        }
+      }
+
+      $value = NULL;
+      // Use prettified string representation.
+      if ($flat_value !== NULL) {
+        $value = $flat_value;
+      }
+      // Use var_export() for Booleans.
+      // Do not output NULL values on the top-level to allow for labels without
+      // following value.
+      elseif ($array !== NULL) {
+        $value = var_export($array, TRUE);
+      }
+
+      // Inject all other key/value pairs as @headingN (and optional
+      // '<pre>@valueN</pre>') placeholders.
+      if (isset($value)) {
+        $message .= "@heading{$i}\n<pre>@value{$i}</pre>\n";
+        $arguments += array(
+          '@heading' . $i => $token,
+          '@value' . $i => $value,
+        );
+      }
+      else {
+        $message .= "<p>@heading{$i}</p>\n";
+        $arguments += array(
+          '@heading' . $i => $token,
+        );
+      }
+      $i++;
+    }
+  }
+  return array($message, $arguments);
+}
+
+/**
+ * Replaces placeholders with sanitized values in a string.
+ *
+ * @param $string
+ *   A string containing placeholders.
+ * @param $args
+ *   An associative array of replacements to make. Occurrences in $string of
+ *   any key in $args are replaced with the corresponding value, after
+ *   sanitization. The sanitization function depends on the first character of
+ *   the key:
+ *   - !variable: Inserted as is. Use this for text that has already been
+ *     sanitized.
+ *   - @variable: Escaped to HTML using check_plain(). Use this for anything
+ *     displayed on a page on the site.
+ *   - %variable: Escaped as a placeholder for user-submitted content using
+ *     drupal_placeholder(), which shows up as <em>emphasized</em> text.
+ *
+ * @see t()
+ * @ingroup sanitization
+ */
+function _mollom_format_string($string, array $args = array()) {
+  // Transform arguments before inserting them.
+  foreach ($args as $key => $value) {
+    switch ($key[0]) {
+      case '@':
+        // Escaped only.
+        $args[$key] = check_plain($value);
+        break;
+
+      case '%':
+      default:
+        // Escaped and placeholder.
+        $args[$key] = drupal_placeholder($value);
+        break;
+
+      case '!':
+        // Pass-through.
+    }
+  }
+  return strtr($string, $args);
+}
+
+/**
  * Log a Mollom system message.
  *
  * @param $parts
@@ -1956,14 +2189,13 @@ function _mollom_send_feedback($data, $reason = 'spam') {
     'reason' => $reason,
   );
   $result = mollom()->sendFeedback($params);
-  _mollom_watchdog(array(
-    'Reported %feedback for @resource %id.' => array(
+  mollom_log(array(
+    'message' => 'Reported %feedback for @resource %id.',
+    'arguments' => array(
       '%feedback' => $reason,
       '@resource' => $resource,
       '%id' => $id,
     ),
-    'Data:<pre>@data</pre>' => array('@data' => $params),
-    'Result:<pre>@result</pre>' => array('@result' => $result),
   ));
   return $result;
 }
@@ -2267,11 +2499,9 @@ function mollom_mollom_data_insert($data) {
       ->execute();
   }
 
-  // @todo Should be kept, but don't log debug messages in production.
-  _mollom_watchdog(array(
-    'Stored @entity @id.' => array('@entity' => $data->entity, '@id' => $data->id),
-    'Data:<pre>@data</pre>' => array('@data' => $params),
-    'Result:<pre>@result</pre>' => array('@result' => $result),
+  mollom_log(array(
+    'message' => 'Stored @entity @id.',
+    'arguments' => array('@entity' => $data->entity, '@id' => $data->id),
   ), WATCHDOG_DEBUG);
 }
 
@@ -2300,10 +2530,9 @@ function mollom_mollom_data_delete($data) {
 
   $result = mollom()->checkContent($params);
 
-  _mollom_watchdog(array(
-    'Deleted @entity @id.' => array('@entity' => $data->entity, '@id' => $data->id),
-    'Data:<pre>@data</pre>' => array('@data' => $params),
-    'Result:<pre>@result</pre>' => array('@result' => $result),
+  mollom_log(array(
+    'message' => 'Deleted @entity @id.',
+    'arguments' => array('@entity' => $data->entity, '@id' => $data->id),
   ), WATCHDOG_DEBUG);
 }
 
diff --git a/tests/mollom.test b/tests/mollom.test
index 66282b5..fdc46e8 100644
--- a/tests/mollom.test
+++ b/tests/mollom.test
@@ -188,6 +188,11 @@ class MollomWebTestCase extends DrupalWebTestCase {
    * @todo Add this to Drupal core.
    */
   protected function assertMollomWatchdogMessages($max_severity = WATCHDOG_NOTICE) {
+    // Ensure that all messages have been written before attempting to verify
+    // them. Actions executed within the test class may lead to log messages,
+    // but those get only logged when hook_exit() is triggered.
+    mollom_log_write();
+
     module_load_include('inc', 'dblog', 'dblog.admin');
 
     $this->messages = array();
