diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index f546898..d7f86f7 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,10 @@
+jsonlog 7.x-2.x, 2015-03-21
+---------------------------
+- Replace variables in message (issue #2448753). And do it always; not optional.
+- Support reverse proxy forwarded client IP, even if the equivalent Drupal core
+  conf settings aren't defined (issue #2427723).
+- Now truncates client IP and link properties like dblog do.
+
 jsonlog 7.x-2.x, 2014-12-16
 ---------------------------
 * Don't escape newlines, (drupal_)json_encode() does that.
diff --git a/jsonlog.admin.css b/jsonlog.admin.css
index beec6b3..2e83f22 100644
--- a/jsonlog.admin.css
+++ b/jsonlog.admin.css
@@ -40,3 +40,12 @@ div.form-item.form-item-jsonlog-test-filing div.description {
   margin-left: 0.5em;
   color: #EEE;
 }
+
+div.form-type-checkbox.form-item-jsonlog-replace-variables label {
+  font-weight: bold;
+  font-size: 1em;
+}
+
+div.form-item.jsonlog-fieldlike-paragraph p {
+  margin: 0.1em 0 0;
+}
diff --git a/jsonlog.inc b/jsonlog.inc
index 3fc72e7..3efd3b3 100644
--- a/jsonlog.inc
+++ b/jsonlog.inc
@@ -334,6 +334,43 @@ function _jsonlog_form_system_logging_settings_alter(&$form, &$form_state) {
     '#collapsed' => FALSE,
   );
 
+  // Reverse proxy - display on/off state only.
+  if (variable_get('reverse_proxy', 0)) {
+    $reverse_proxy = t('Is handled by Drupal core; $conf variable \'reverse_proxy\' is truthy.', array(), array('context' => 'module:jsonlog'));
+  }
+  elseif (($reverse_proxy_header = getenv('drupal_jsonlog_reverse_proxy_header'))
+    && !empty($_SERVER[$reverse_proxy_header])
+    && ($reverse_proxy_addresses = getenv('drupal_jsonlog_reverse_proxy_addresses'))
+    && explode(',', str_replace(' ', '', $reverse_proxy_addresses))
+  ) {
+    $reverse_proxy = t(
+      'Is enabled and defined via server environment variables \'drupal_jsonlog_reverse_proxy_header\' and  \'drupal_jsonlog_reverse_proxy_addresses\'.',
+      array(), array('context' => 'module:jsonlog')
+    );
+  }
+  else {
+    $reverse_proxy = t(
+      'Is off, by Drupal core as well as JSONlog server environment variables.',
+      array(), array('context' => 'module:jsonlog')
+    );;
+  }
+  $form['jsonlog']['reverse_proxy'] = array(
+    '#type' => 'markup',
+    '#markup' => '<div class="form-item jsonlog-fieldlike-paragraph">'
+      . '<label>'
+      . t('Reverse proxy client I.P. awareness', array(), array('context' => 'module:jsonlog'))
+      . '</label>'
+      . '<p>' . $reverse_proxy .'</p>'
+      . '<div class="description">'
+      . t(
+        'Reverse proxy forwarded client I.P. address awareness may be enabled by setting server environment variables \'drupal_jsonlog_reverse_proxy_header\' (to a value like \'HTTP_X_FORWARDED_FOR\')!nland \'drupal_jsonlog_reverse_proxy_addresses\' (space- or comma-separated list of I.P. addresses).!nlHowever, those env. vars !emphwill be ignored!_emph if the Drupal $conf variable \'reverse_proxy\' is set (to truthy), because it doesn\'t make sense to resolve the forwarded client I.P. twice.',
+        array('!nl' => '<br/>', '!emph' => '<em>', '!_emph' => '</em>'),
+        array('context' => 'module:jsonlog')
+      )
+      . '</div>'
+      . '</div>',
+  );
+
   // Severity threshold.
   if (($severity_threshold = getenv('drupal_jsonlog_severity_threshold'))) {
     $overridden = TRUE;
@@ -449,7 +486,7 @@ function _jsonlog_form_system_logging_settings_alter(&$form, &$form_state) {
       array('context' => 'module:jsonlog')
     ),
     '#options' => array(
-      // Deliberaterately not '_none'.
+      // Deliberately not '_none'.
       'none' => t('None - use the same file forever', array(), array('context' => 'module:jsonlog')),
       'Ymd' => t('Day (\'Ymd\' ~ YYYYMMDD)', array(), array('context' => 'module:jsonlog')),
       'YW' => t('Week (\'YW\' ~ YYYYWW)', array(), array('context' => 'module:jsonlog')),
@@ -523,7 +560,6 @@ function _jsonlog_form_system_logging_settings_alter(&$form, &$form_state) {
     '#field_prefix' => $tags_server !== '' ? ($tags_server . ', ') : '',
   );
 
-
   // Make table view of a log entry.
 
   // ISO 8601 timestamp.
@@ -631,17 +667,17 @@ function _jsonlog_form_system_logging_settings_alter(&$form, &$form_state) {
     ),
     'link' => array(
       'label' => t('Link', array(), array('context' => 'module:jsonlog')),
-      'value' => '',
+      'value' => 'NULL',
     ),
     'variables' => array(
-      'label' => t('Variables (null or object hash map)', array(), array('context' => 'module:jsonlog')),
-      'value' => '',
+      'label' => t('Variables (always null, because they get replaced into the message)', array(), array('context' => 'module:jsonlog')),
+      'value' => 'NULL',
     ),
     'truncation' => array(
       'custom' => TRUE,
       'name' => 'trunc',
       'label' => t('Truncation (null, or array [original message length, truncated message length])', array(), array('context' => 'module:jsonlog')),
-      'value' => NULL,
+      'value' => 'NULL',
     ),
   );
 
diff --git a/jsonlog.module b/jsonlog.module
index 5d2bf85..18228ca 100644
--- a/jsonlog.module
+++ b/jsonlog.module
@@ -129,10 +129,13 @@ function jsonlog_form_system_logging_settings_submit($form, &$form_state) {
  *
  * Implements hook_watchdog().
  *
+ * @see ip_address()
+ * @see format_string()
+ *
  * @param array $log_entry
  */
 function jsonlog_watchdog(array $log_entry) {
-  static $_threshold, $_site_id, $_file, $_truncate, $_severity, $_tags;
+  static $_threshold, $_site_id, $_file, $_truncate, $_severity, $_tags, $ip_address;
 
   // Don't load more settings than threshold, in case current entry isn't sufficiently severe.
   if (!$_threshold) {
@@ -214,11 +217,44 @@ function jsonlog_watchdog(array $log_entry) {
     if ($tags) {
       $_tags = explode(',', $tags);
     }
+
+    // Establish client IP once; it won't change during request processing
+    // (unless something fiddles with Drupal settings or the $_SERVER array).
+    $ip_address = substr($log_entry['ip'], 0, 128);
+    // Use JSONlog's reverse proxy header settings unless Drupal core's ditto
+    // are set/enabled.
+    if (!variable_get('reverse_proxy', 0)
+      && ($reverse_proxy_header = getenv('drupal_jsonlog_reverse_proxy_header'))
+      && !empty($_SERVER[$reverse_proxy_header])
+      && ($reverse_proxy_addresses = getenv('drupal_jsonlog_reverse_proxy_addresses'))
+      && ($reverse_proxy_addresses = strpos($reverse_proxy_addresses, ',') ?
+        explode(',', str_replace(' ', '', $reverse_proxy_addresses)) :
+        explode(' ', trim($reverse_proxy_addresses))
+      )
+    ) {
+      // The following is an exact copy of core's ip_address() function.
+
+      // Turn XFF header into an array.
+      $forwarded = explode(',', $_SERVER[$reverse_proxy_header]);
+
+      // Trim the forwarded IPs; they may have been delimited by commas and spaces.
+      $forwarded = array_map('trim', $forwarded);
+
+      // Tack direct client IP onto end of forwarded array.
+      $forwarded[] = $ip_address;
+
+      // Eliminate all trusted IPs.
+      $untrusted = array_diff($forwarded, $reverse_proxy_addresses);
+
+      // The right-most IP is the most specific we can trust.
+      $ip_address = array_pop($untrusted);
+
+      // /The following...
+    }
   }
 
 
   // Create the entry.
-
   $entry = new stdClass();
 
   // Strip tags if message starts with < (Inspect logs in tag).
@@ -228,15 +264,20 @@ function jsonlog_watchdog(array $log_entry) {
   // Escape null byte.
   $message = str_replace("\0", '_NUL_', $message);
 
-  // If truncation required, start by skipping variables.
-  $variables = $log_entry['variables'];
+  // Replace variables.
+  if (($variables = $log_entry['variables'])) {
+    // Doesn't use format_string() because we don't want HTML placeholder tags.
+    foreach ($variables as $key => $value) {
+      if ($key[0] != '!') {
+        $variables[$key] = check_plain($value);
+      }
+    }
+    $message = strtr($message, $variables);
+    unset($variables);
+  }
 
   // Truncate message.
   if ($_truncate && ($le = strlen($message)) > $_truncate) { // Deliberately not drupal_strlen(); need 'physical' length, not (possibly shorter) multibyte length.
-    // Flag variables truncated by setting it to false.
-    if ($variables) {
-      $variables = FALSE;
-    }
     // Truncate multibyte safe until ASCII length is equal to/less than max. byte length.
     $truncation = array(
       $le,
@@ -263,7 +304,7 @@ function jsonlog_watchdog(array $log_entry) {
   $entry->tags = $_tags;
 
   $entry->type = 'drupal';
-  $entry->subtype = $log_entry['type'];
+  $entry->subtype = substr($log_entry['type'], 0, 64);
 
   $entry->severity = $_severity[$log_entry['severity']];
 
@@ -274,9 +315,17 @@ function jsonlog_watchdog(array $log_entry) {
   $entry->uid = $uid = $log_entry['uid'];
   $entry->username = $uid && !empty($GLOBALS['user']->name) ? $GLOBALS['user']->name : '';
 
-  $entry->client_ip = $log_entry['ip'];
-  $entry->link = !$log_entry['link'] ? NULL : strip_tags($log_entry['link']);
-  $entry->variables = $variables ? $variables : NULL;
+  $entry->client_ip = $ip_address;
+
+  $entry->link = !$log_entry['link'] ? NULL : substr(strip_tags($log_entry['link']), 0, 255);
+
+  // Since message/variables replacement was implemented, variables will always
+  // be empty. A bit silly to keep the variables property at all then, but for
+  // backwards compatibility - and the fact that folks might expect the
+  // property to exist because it's part the of the hook_watchdog() properties
+  // - we keep setting it.
+  $entry->variables = NULL;
+
   $entry->trunc = $truncation;
 
 
