You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by rr...@apache.org on 2020/05/08 15:17:00 UTC

[trafficserver] branch master updated: traffic_dump: refactor to make transactions atomically written

This is an automated email from the ASF dual-hosted git repository.

rrm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 59ebfd8  traffic_dump: refactor to make transactions atomically written
59ebfd8 is described below

commit 59ebfd82a67bfdbe323c21cf304d08345540e461
Author: bneradt <bn...@verizonmedia.com>
AuthorDate: Fri May 1 18:55:18 2020 +0000

    traffic_dump: refactor to make transactions atomically written
    
    This refactor is occasioned by the need to make transaction writes
    atomic. Mainly, this encapsulates session and transaction handling
    in SessionData and TransactionData classes.
---
 .gitignore                                         |   1 +
 plugins/experimental/traffic_dump/Makefile.inc     |  28 +-
 .../experimental/traffic_dump/global_variables.h   |  25 +
 plugins/experimental/traffic_dump/json_utils.cc    | 178 ++++
 plugins/experimental/traffic_dump/json_utils.h     |  58 ++
 .../experimental/traffic_dump/sensitive_fields.h   |  54 ++
 plugins/experimental/traffic_dump/session_data.cc  | 486 +++++++++++
 plugins/experimental/traffic_dump/session_data.h   | 182 ++++
 plugins/experimental/traffic_dump/traffic_dump.cc  | 929 ++-------------------
 .../experimental/traffic_dump/transaction_data.cc  | 358 ++++++++
 .../experimental/traffic_dump/transaction_data.h   | 110 +++
 .../traffic_dump/unit_tests/test_json_utils.cc     |  59 ++
 .../unit_tests/test_sensitive_fields.cc            |  37 +
 .../traffic_dump/unit_tests/unit_test_main.cc      |  25 +
 .../pluginTest/traffic_dump/gold/200.gold          |   2 +-
 .../traffic_dump/gold/4_byte_response_body.gold    |   7 +
 .../traffic_dump/gold/two_transactions.gold        |  11 +
 .../pluginTest/traffic_dump/traffic_dump.test.py   | 125 ++-
 .../pluginTest/traffic_dump/verify_replay.py       |   6 +-
 tests/tools/lib/replay_schema.json                 |   2 +-
 20 files changed, 1796 insertions(+), 887 deletions(-)

diff --git a/.gitignore b/.gitignore
index 6642e9a..f03cfb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -131,6 +131,7 @@ plugins/experimental/esi/*_test
 plugins/experimental/slice/test_*
 plugins/experimental/sslheaders/test_sslheaders
 plugins/s3_auth/test_s3auth
+plugins/experimental/traffic_dump/test_*
 
 plugins/esi/docnode_test
 plugins/esi/gzip_test
diff --git a/plugins/experimental/traffic_dump/Makefile.inc b/plugins/experimental/traffic_dump/Makefile.inc
index ac87596..411a9f7 100644
--- a/plugins/experimental/traffic_dump/Makefile.inc
+++ b/plugins/experimental/traffic_dump/Makefile.inc
@@ -16,4 +16,30 @@
 
 pkglib_LTLIBRARIES += experimental/traffic_dump/traffic_dump.la
 
-experimental_traffic_dump_traffic_dump_la_SOURCES = experimental/traffic_dump/traffic_dump.cc
+experimental_traffic_dump_traffic_dump_la_SOURCES = \
+        experimental/traffic_dump/global_variables.h \
+        experimental/traffic_dump/json_utils.cc \
+        experimental/traffic_dump/json_utils.h \
+        experimental/traffic_dump/sensitive_fields.h \
+        experimental/traffic_dump/session_data.cc \
+        experimental/traffic_dump/session_data.h \
+        experimental/traffic_dump/traffic_dump.cc \
+        experimental/traffic_dump/transaction_data.cc \
+        experimental/traffic_dump/transaction_data.h
+
+check_PROGRAMS += \
+        experimental/traffic_dump/test_traffic_dump
+
+experimental_traffic_dump_test_traffic_dump_CPPFLAGS = \
+        $(AM_CPPFLAGS) \
+        -I$(abs_top_srcdir)/plugins/experimental/traffic_dump \
+        -I$(abs_top_srcdir)/tests/include
+
+experimental_traffic_dump_test_traffic_dump_SOURCES = \
+	experimental/traffic_dump/unit_tests/unit_test_main.cc \
+        experimental/traffic_dump/unit_tests/test_json_utils.cc \
+        experimental/traffic_dump/unit_tests/test_sensitive_fields.cc \
+        experimental/traffic_dump/json_utils.cc \
+        experimental/traffic_dump/sensitive_fields.h
+
+# vim: ft=make ts=8 sw=8 et:
diff --git a/plugins/experimental/traffic_dump/global_variables.h b/plugins/experimental/traffic_dump/global_variables.h
new file mode 100644
index 0000000..204ce20
--- /dev/null
+++ b/plugins/experimental/traffic_dump/global_variables.h
@@ -0,0 +1,25 @@
+/** @sensitive_fields.h
+  The set of fields considered user-sensitive.
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#pragma once
+
+namespace traffic_dump
+{
+char const constexpr *const debug_tag = "traffic_dump";
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/json_utils.cc b/plugins/experimental/traffic_dump/json_utils.cc
new file mode 100644
index 0000000..eb2ff37
--- /dev/null
+++ b/plugins/experimental/traffic_dump/json_utils.cc
@@ -0,0 +1,178 @@
+/** @json_utils.cc
+  Implementation of JSON formatting functions.
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <iomanip>
+#include <sstream>
+
+#include "json_utils.h"
+
+namespace
+{
+/** Write the content of buf to jsonfile between prevIdx (inclusive) and idx (not
+ * inclusive).
+ *
+ * This function is used to help deal with escaped characters with a low number
+ * of write calls. This is meant to be used like so: the caller inspects every
+ * character in a buffer, doing one of two things with each character:
+ *
+ *  - The character does not need to be escaped. In this case, no call to
+ *  write_buffered_content is made and the idx is advanced and prevIdx is not
+ *  advanced. Eventually this character will be written once the contiguous set
+ *  of non-escaped characters is collected.
+ *
+ *  - The character needs to be escaped. In this case, it will call this
+ *  function. Anything between prevIdx and up to (but not including) idx is
+ *  written. The caller then writes the escaped sequence for the character
+ *  pointed to by idx ("\\t" instead of '\t', for instance). Then prevIdx is set
+ *  past the escaped character.
+ *
+ * Finally, once all characters are inspected, a final write_buffered_content
+ * call is made with idx set to one past the last character in buf. This
+ * results in all characters between prevIdx and the last character in buf
+ * (including that character) to be written.
+ *
+ * @param[in] buf The buffer containing characters that need to be written to jsonfile.
+ *
+ * @param[in,out] prevIdx The pointer to the beginning of the set of characters
+ * in buf that have not been written yet. This is always updated before
+ * function exit to one past idx.
+ *
+ * @param[in] idx The current character in buf being inspected.
+ *
+ * @param[out] jsonfile The stream to conditionally write buffer content to.
+ */
+inline void
+write_buffered_context(char const *buf, int64_t &prevIdx, int64_t idx, std::ostream &jsonfile)
+{
+  if (prevIdx < idx) {
+    jsonfile.write(buf + prevIdx, idx - prevIdx);
+  }
+  prevIdx = idx + 1;
+}
+
+int
+esc_json_out(const char *buf, int64_t len, std::ostream &jsonfile)
+{
+  if (buf == nullptr) {
+    return 0;
+  }
+  int64_t idx = 0, prevIdx = 0;
+  // For an explanation of the algorithm here, see the doxygen comment for
+  // write_buffered_content.
+  for (idx = 0; idx < len; idx++) {
+    char c = *(buf + idx);
+    switch (c) {
+    case '"':
+    case '\\': {
+      write_buffered_context(buf, prevIdx, idx, jsonfile);
+      jsonfile << "\\" << c;
+      break;
+    }
+    case '\b': {
+      write_buffered_context(buf, prevIdx, idx, jsonfile);
+      jsonfile << "\\b";
+      break;
+    }
+    case '\f': {
+      write_buffered_context(buf, prevIdx, idx, jsonfile);
+      jsonfile << "\\f";
+      break;
+    }
+    case '\n': {
+      write_buffered_context(buf, prevIdx, idx, jsonfile);
+      jsonfile << "\\n";
+      break;
+    }
+    case '\r': {
+      write_buffered_context(buf, prevIdx, idx, jsonfile);
+      jsonfile << "\\r";
+      break;
+    }
+    case '\t': {
+      write_buffered_context(buf, prevIdx, idx, jsonfile);
+      jsonfile << "\\t";
+      break;
+    }
+    default: {
+      if ('\x00' <= c && c <= '\x1f') {
+        write_buffered_context(buf, prevIdx, idx, jsonfile);
+        jsonfile << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(c);
+      }
+      break;
+      // else: The character does not need to be escaped. Do not call
+      // write_buffered_content so nothing is written and prevIdx remains
+      // pointing to the first character that needs to be written on the next
+      // call to write_buffered_content.
+    }
+    }
+  }
+  // Finally, call write_buffered_content to write out any data that has not
+  // been written yet.
+  write_buffered_context(buf, prevIdx, idx, jsonfile);
+
+  return len;
+}
+/** Escape characters in a string as needed and return the resultant escaped string.
+ *
+ * @param[in] s The characters that need to be escaped.
+ */
+std::string
+escape_json(std::string_view s)
+{
+  std::ostringstream o;
+  esc_json_out(s.data(), s.length(), o);
+  return o.str();
+}
+
+/** An escape_json overload for a char buffer.
+ *
+ * @param[in] buf The char buffer pointer with characters that need to be escaped.
+ *
+ * @param[in] size The size of the buf char array.
+ */
+std::string
+escape_json(char const *buf, int64_t size)
+{
+  std::ostringstream o;
+  esc_json_out(buf, size, o);
+  return o.str();
+}
+
+} // anonymous namespace
+
+namespace traffic_dump
+{
+std::string
+json_entry(std::string_view name, std::string_view value)
+{
+  return "\"" + escape_json(name) + "\":\"" + escape_json(value) + "\"";
+}
+
+std::string
+json_entry(std::string_view name, char const *value, int64_t size)
+{
+  return "\"" + escape_json(name) + "\":\"" + escape_json(value, size) + "\"";
+}
+
+std::string
+json_entry_array(std::string_view name, std::string_view value)
+{
+  return "[\"" + escape_json(name) + "\",\"" + escape_json(value) + "\"]";
+}
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/json_utils.h b/plugins/experimental/traffic_dump/json_utils.h
new file mode 100644
index 0000000..7d26012
--- /dev/null
+++ b/plugins/experimental/traffic_dump/json_utils.h
@@ -0,0 +1,58 @@
+/** @json_utils.h
+  JSON formatting functions.
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+namespace traffic_dump
+{
+/** Create the name and value as an escaped JSON map entry.
+ *
+ * @param[in] name The key name for the map entry.
+ *
+ * @param[in] value The value to write.
+ *
+ * @return The JSON map string.
+ */
+std::string json_entry(std::string_view name, std::string_view value);
+
+/** Create the name and value as an escaped JSON map entry.
+ *
+ * @param[in] name The key name for the map entry.
+ *
+ * @param[in] value The buffer for the value to write.
+ *
+ * @param[in] size The size of the value buffer.
+ *
+ * @return The JSON map string.
+ */
+std::string json_entry(std::string_view name, char const *value, int64_t size);
+
+/** Create the name and value as an escaped JSON array entry.
+ *
+ * @param[in] name The key name for the map entry.
+ *
+ * @param[in] value The value to write for the JSON map entry.
+ *
+ * @return The JSON array string.
+ */
+std::string json_entry_array(std::string_view name, std::string_view value);
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/sensitive_fields.h b/plugins/experimental/traffic_dump/sensitive_fields.h
new file mode 100644
index 0000000..8e0db2d
--- /dev/null
+++ b/plugins/experimental/traffic_dump/sensitive_fields.h
@@ -0,0 +1,54 @@
+/** @sensitive_fields.h
+  Define the type used to store user-sensitive HTTP fields (such as cookies).
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <strings.h>
+#include <string>
+#include <string_view>
+#include <unordered_set>
+
+namespace traffic_dump
+{
+// A case-insensitive comparitor used for comparing HTTP field names.
+struct InsensitiveCompare {
+  bool
+  operator()(std::string_view a, std::string_view b) const
+  {
+    return strcasecmp(a.data(), b.data()) == 0;
+  }
+};
+
+// A case-insensitive hash functor for HTTP field names.
+struct StringHashByLower {
+public:
+  size_t
+  operator()(std::string_view str) const
+  {
+    std::string lower;
+    std::transform(str.begin(), str.end(), lower.begin(), [](unsigned char c) -> unsigned char { return std::tolower(c); });
+    return std::hash<std::string>()(lower);
+  }
+};
+
+/** The type used to store the set of user-sensitive HTTP fields, such as
+ * "Cookie" and "Set-Cookie". */
+using sensitive_fields_t = std::unordered_set<std::string, StringHashByLower, InsensitiveCompare>;
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/session_data.cc b/plugins/experimental/traffic_dump/session_data.cc
new file mode 100644
index 0000000..ffbdb50
--- /dev/null
+++ b/plugins/experimental/traffic_dump/session_data.cc
@@ -0,0 +1,486 @@
+/** @session_handler.h
+  Traffic Dump session handling implementation
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <arpa/inet.h>
+#include <chrono>
+#include <fcntl.h>
+#include <iomanip>
+#include <openssl/ssl.h>
+#include <netinet/in.h>
+#include <sstream>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "session_data.h"
+#include "global_variables.h"
+#include "transaction_data.h"
+
+namespace
+{
+/** The final string used to close a JSON session. */
+char const constexpr *const json_closing = "]}]}";
+} // namespace
+
+namespace traffic_dump
+{
+// Static member initialization.
+int SessionData::session_arg_index                 = -1;
+std::atomic<int64_t> SessionData::sample_pool_size = default_sample_pool_size;
+std::atomic<int64_t> SessionData::max_disk_usage   = default_max_disk_usage;
+std::atomic<int64_t> SessionData::disk_usage       = 0;
+ts::file::path SessionData::log_directory{default_log_directory};
+uint64_t SessionData::session_counter = 0;
+std::string SessionData::sni_filter;
+
+int
+SessionData::get_session_arg_index()
+{
+  return session_arg_index;
+}
+
+void
+SessionData::set_sample_pool_size(int64_t new_sample_size)
+{
+  sample_pool_size = new_sample_size;
+}
+
+void
+SessionData::reset_disk_usage()
+{
+  disk_usage = 0;
+}
+
+void
+SessionData::set_max_disk_usage(int64_t new_max_disk_usage)
+{
+  max_disk_usage = new_max_disk_usage;
+}
+
+bool
+SessionData::init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size)
+{
+  SessionData::log_directory    = log_directory;
+  SessionData::max_disk_usage   = max_disk_usage;
+  SessionData::sample_pool_size = sample_size;
+
+  if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_SSN, debug_tag, "Track log related data", &session_arg_index)) {
+    TSError("[%s] Unable to initialize plugin (disabled). Failed to reserve ssn arg.", traffic_dump::debug_tag);
+    return false;
+  }
+
+  TSCont ssncont = TSContCreate(global_session_handler, nullptr);
+  TSHttpHookAdd(TS_HTTP_SSN_START_HOOK, ssncont);
+  TSHttpHookAdd(TS_HTTP_SSN_CLOSE_HOOK, ssncont);
+
+  TSDebug(debug_tag, "Initialized with log directory: %s", SessionData::log_directory.c_str());
+  TSDebug(debug_tag, "Initialized with sample pool size %" PRId64 " bytes and disk limit %" PRId64 " bytes", sample_size,
+          max_disk_usage);
+  return true;
+}
+
+bool
+SessionData::init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size, std::string_view sni_filter)
+{
+  if (!init(log_directory, max_disk_usage, sample_size)) {
+    return false;
+  }
+  SessionData::sni_filter = sni_filter;
+  TSDebug(debug_tag, "Filtering to only dump connections with SNI: %s", SessionData::sni_filter.c_str());
+  return true;
+}
+
+/** Create a TLS characteristics node.
+ *
+ * This function encapsulates the logic common between the client-side and
+ * server-side logic for populating the TLS node.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The node describing the TLS properties of this session.
+ */
+std::string
+get_tls_description_helper(TSVConn ssn_vc)
+{
+  TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
+  SSL *ssl_obj             = (SSL *)ssl_conn;
+  if (ssl_obj == nullptr) {
+    return "";
+  }
+  std::ostringstream tls_description;
+  tls_description << R"("tls":{)";
+  char const *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
+  if (sni_ptr != nullptr) {
+    std::string_view sni{sni_ptr};
+    if (!sni.empty()) {
+      tls_description << R"("sni":")" << sni << R"(",)";
+    }
+  }
+  tls_description << R"("verify_mode":")" << std::to_string(SSL_get_verify_mode(ssl_obj)) << R"(")";
+  tls_description << "}";
+  return tls_description.str();
+}
+
+/** Create a server-side TLS characteristics node.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The node describing the TLS properties of this session.
+ */
+std::string
+get_server_tls_description(TSHttpSsn ssnp)
+{
+  TSVConn ssn_vc = TSHttpSsnServerVConnGet(ssnp);
+  return get_tls_description_helper(ssn_vc);
+}
+
+/** Create a client-side TLS characteristics node.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The node describing the TLS properties of this session.
+ */
+std::string
+get_client_tls_description(TSHttpSsn ssnp)
+{
+  TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp);
+  return get_tls_description_helper(ssn_vc);
+}
+
+/// A named boolean for callers who pass the is_client parameter.
+constexpr bool IS_CLIENT = true;
+
+/** Create the nodes that describe the session's sub-HTTP protocols.
+ *
+ * This function encapsulates the logic common between the client-side and
+ * server-side logic for describing the session's characteristics.
+ *
+ * This will create the string representing the "protocol" and "tls" nodes. The
+ * "tls" node will only be present if the connection is over SSL/TLS.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The description of the protocol stack and certain TLS attributes.
+ */
+std::string
+get_protocol_description_helper(TSHttpSsn ssnp, bool is_client)
+{
+  std::ostringstream protocol_description;
+  protocol_description << R"("protocol":[)";
+
+  char const *protocol[10];
+  int count = -1;
+  if (is_client) {
+    TSAssert(TS_SUCCESS == TSHttpSsnClientProtocolStackGet(ssnp, 10, protocol, &count));
+  } else {
+    // See the TODO below in the commented out defintion of get_server_protocol_description.
+    // TSAssert(TS_SUCCESS == TSHttpSsnServerProtocolStackGet(ssnp, 10, protocol, &count));
+  }
+  for (int i = 0; i < count; i++) {
+    if (i > 0) {
+      protocol_description << ",";
+    }
+    protocol_description << '"' << std::string(protocol[i]) << '"';
+  }
+
+  protocol_description << "]";
+  std::string tls_description;
+  if (is_client) {
+    tls_description = get_client_tls_description(ssnp);
+  } else {
+    tls_description = get_server_tls_description(ssnp);
+  }
+  if (!tls_description.empty()) {
+    protocol_description << "," << tls_description;
+  }
+  return protocol_description.str();
+}
+
+#if 0
+// See the TODO above the get_server_protocol_description declaration.
+//
+// It will be important to add this eventually, but
+// TSHttpSsnServerProtocolStackGet is not defined yet. Once it (or some other
+// mechanism for getting the server side stack) is implemented, we will call
+// this as a part of writing the server-response node.
+std::string
+SessionData::get_server_protocol_description(TSHttpSsn ssnp)
+{
+  return get_protocol_description_helper(ssnp, !IS_CLIENT);
+}
+#endif
+
+std::string
+SessionData::get_client_protocol_description(TSHttpSsn ssnp)
+{
+  return get_protocol_description_helper(ssnp, IS_CLIENT);
+}
+
+SessionData::SessionData()
+{
+  disk_io_mutex = TSMutexCreate();
+  aio_cont      = TSContCreate(session_aio_handler, TSMutexCreate());
+  txn_cont      = TSContCreate(TransactionData::global_transaction_handler, nullptr);
+}
+
+SessionData::~SessionData()
+{
+  if (disk_io_mutex) {
+    TSMutexDestroy(disk_io_mutex);
+  }
+  if (aio_cont) {
+    TSContDestroy(aio_cont);
+  }
+  if (txn_cont) {
+    TSContDestroy(txn_cont);
+  }
+}
+
+/*
+ * Note this assumes that the caller holds the disk_io_mutex lock. This is a
+ * private member function. The two publically accessible functions hold the
+ * lock before calling this.
+ */
+int
+SessionData::write_to_disk_no_lock(std::string_view content)
+{
+  char *pBuf = nullptr;
+  // Allocate a buffer for aio writing
+  if ((pBuf = static_cast<char *>(TSmalloc(sizeof(char) * content.size())))) {
+    memcpy(pBuf, content.data(), content.size());
+    if (TS_SUCCESS == TSAIOWrite(log_fd, write_offset, pBuf, content.size(), aio_cont)) {
+      // Update offset within file and aio events count
+      write_offset += content.size();
+      aio_count += 1;
+
+      TSMutexUnlock(disk_io_mutex);
+      return TS_SUCCESS;
+    }
+    TSfree(pBuf);
+  }
+  return TS_ERROR;
+}
+
+int
+SessionData::write_to_disk(std::string_view content)
+{
+  TSMutexLock(disk_io_mutex);
+  const int result = write_to_disk_no_lock(content);
+  TSMutexUnlock(disk_io_mutex);
+  return result;
+}
+
+int
+SessionData::write_transaction_to_disk(std::string_view content)
+{
+  TSMutexLock(disk_io_mutex);
+
+  int result = TS_SUCCESS;
+  if (has_written_first_transaction) {
+    // Prepend a comma.
+    std::string with_comma;
+    with_comma.reserve(content.size() + 1);
+    with_comma.insert(0, ",");
+    with_comma.insert(1, content);
+    result = write_to_disk_no_lock(with_comma);
+  } else {
+    result                        = write_to_disk_no_lock(content);
+    has_written_first_transaction = true;
+  }
+
+  TSMutexUnlock(disk_io_mutex);
+  return result;
+}
+
+int
+SessionData::session_aio_handler(TSCont contp, TSEvent event, void *edata)
+{
+  switch (event) {
+  case TS_EVENT_AIO_DONE: {
+    TSAIOCallback cb     = static_cast<TSAIOCallback>(edata);
+    SessionData *ssnData = static_cast<SessionData *>(TSContDataGet(contp));
+    if (!ssnData) {
+      TSDebug(debug_tag, "session_aio_handler(): No valid ssnData. Abort.");
+      return TS_ERROR;
+    }
+    char *buf = TSAIOBufGet(cb);
+    TSMutexLock(ssnData->disk_io_mutex);
+
+    // Free the allocated buffer and update aio_count
+    if (buf) {
+      TSfree(buf);
+      if (--ssnData->aio_count == 0 && ssnData->ssn_closed) {
+        // check for ssn close, if closed, do clean up
+        TSContDataSet(contp, nullptr);
+        close(ssnData->log_fd);
+        TSMutexUnlock(ssnData->disk_io_mutex);
+        std::error_code ec;
+        ts::file::file_status st = ts::file::status(ssnData->log_name, ec);
+        if (!ec) {
+          disk_usage += ts::file::file_size(st);
+          TSDebug(debug_tag, "Finish a session with log file of %" PRIuMAX "bytes", ts::file::file_size(st));
+        }
+        delete ssnData;
+        return TS_SUCCESS;
+      }
+    }
+    TSMutexUnlock(ssnData->disk_io_mutex);
+    return TS_SUCCESS;
+  }
+  default:
+    TSDebug(debug_tag, "session_aio_handler(): unhandled events %d", event);
+    return TS_ERROR;
+  }
+  return TS_SUCCESS;
+}
+
+int
+SessionData::global_session_handler(TSCont contp, TSEvent event, void *edata)
+{
+  TSHttpSsn ssnp = static_cast<TSHttpSsn>(edata);
+
+  switch (event) {
+  case TS_EVENT_HTTP_SSN_START: {
+    // Grab session id for logging against a global value rather than the local
+    // session_counter.
+    int64_t id = TSHttpSsnIdGet(ssnp);
+
+    // If the user has asked for SNI filtering, filter on that first because
+    // any sampling will apply just to that subset of connections that match
+    // that SNI.
+    if (!sni_filter.empty()) {
+      TSVConn ssn_vc           = TSHttpSsnClientVConnGet(ssnp);
+      TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
+      SSL *ssl_obj             = (SSL *)ssl_conn;
+      if (ssl_obj == nullptr) {
+        TSDebug(debug_tag, "global_session_handler(): Ignore non-HTTPS session %" PRId64 "...", id);
+        break;
+      }
+      char const *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
+      if (sni_ptr == nullptr) {
+        TSDebug(debug_tag, "global_session_handler(): Ignore HTTPS session with non-existent SNI.");
+        break;
+      } else {
+        const std::string_view sni{sni_ptr};
+        if (sni != sni_filter) {
+          TSDebug(debug_tag, "global_session_handler(): Ignore HTTPS session with non-filtered SNI: %s", sni_ptr);
+          break;
+        }
+      }
+    }
+    const auto this_session_count = session_counter++;
+    if (this_session_count % sample_pool_size != 0) {
+      TSDebug(debug_tag, "global_session_handler(): Ignore session %" PRId64 "...", id);
+      break;
+    } else if (disk_usage >= max_disk_usage) {
+      TSDebug(debug_tag, "global_session_handler(): Ignore session %" PRId64 "due to disk usage %" PRId64 "bytes", id,
+              disk_usage.load());
+      break;
+    }
+    // Beginning of a new session
+    /// Get epoch time
+    auto start = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch());
+
+    // Create new per session data
+    SessionData *ssnData = new SessionData;
+    TSUserArgSet(ssnp, session_arg_index, ssnData);
+
+    TSContDataSet(ssnData->aio_cont, ssnData);
+
+    // "protocol":(string),"tls":(string)
+    // The "tls" node will only be present if the session is over SSL/TLS.
+    std::string protocol_description = get_client_protocol_description(ssnp);
+
+    std::string beginning = R"({"meta":{"version":"1.0"},"sessions":[{)" + protocol_description + R"(,"connection-time":)" +
+                            std::to_string(start.count()) + R"(,"transactions":[)";
+
+    // Use the session count's hex string as the filename.
+    std::stringstream stream;
+    stream << std::setw(16) << std::setfill('0') << std::hex << this_session_count;
+    std::string session_hex_name = stream.str();
+
+    // Use client ip as sub directory name
+    char client_str[INET6_ADDRSTRLEN];
+    sockaddr const *client_ip = TSHttpSsnClientAddrGet(ssnp);
+    if (AF_INET == client_ip->sa_family) {
+      inet_ntop(AF_INET, &(reinterpret_cast<sockaddr_in const *>(client_ip)->sin_addr), client_str, INET_ADDRSTRLEN);
+    } else if (AF_INET6 == client_ip->sa_family) {
+      inet_ntop(AF_INET6, &(reinterpret_cast<sockaddr_in6 const *>(client_ip)->sin6_addr), client_str, INET6_ADDRSTRLEN);
+    } else {
+      TSDebug(debug_tag, "global_session_handler(): Unknown address family.");
+      snprintf(client_str, INET6_ADDRSTRLEN, "unknown");
+    }
+
+    // Initialize AIO file
+    TSMutexLock(ssnData->disk_io_mutex);
+    if (ssnData->log_fd < 0) {
+      ts::file::path log_p = log_directory / ts::file::path(std::string(client_str, 3));
+      ts::file::path log_f = log_p / ts::file::path(session_hex_name);
+
+      // Create subdir if not existing
+      std::error_code ec;
+      ts::file::status(log_p, ec);
+      if (ec && mkdir(log_p.c_str(), 0755) == -1) {
+        TSDebug(debug_tag, "global_session_handler(): Failed to create dir %s", log_p.c_str());
+        TSError("[%s] Failed to create dir %s", debug_tag, log_p.c_str());
+      }
+
+      // Try to open log files for AIO
+      ssnData->log_fd = open(log_f.c_str(), O_RDWR | O_CREAT, S_IRWXU);
+      if (ssnData->log_fd < 0) {
+        TSMutexUnlock(ssnData->disk_io_mutex);
+        TSDebug(debug_tag, "global_session_handler(): Failed to open log files %s. Abort.", log_f.c_str());
+        TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+        return TS_EVENT_HTTP_CONTINUE;
+      }
+      ssnData->log_name = log_f;
+      // Write log file beginning to disk
+      ssnData->write_to_disk(beginning);
+    }
+    TSMutexUnlock(ssnData->disk_io_mutex);
+
+    TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_START_HOOK, ssnData->txn_cont);
+    TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_CLOSE_HOOK, ssnData->txn_cont);
+    break;
+  }
+  case TS_EVENT_HTTP_SSN_CLOSE: {
+    // Write session and close the log file.
+    int64_t id = TSHttpSsnIdGet(ssnp);
+    TSDebug(debug_tag, "global_session_handler(): Closing session %" PRId64 "...", id);
+    // Retrieve SessionData
+    SessionData *ssnData = static_cast<SessionData *>(TSUserArgGet(ssnp, session_arg_index));
+    // If no valid ssnData, continue transaction as if nothing happened
+    if (!ssnData) {
+      TSDebug(debug_tag, "global_session_handler(): [TS_EVENT_HTTP_SSN_CLOSE] No ssnData found. Abort.");
+      TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+      return TS_SUCCESS;
+    }
+    ssnData->write_to_disk(json_closing);
+    TSMutexLock(ssnData->disk_io_mutex);
+    ssnData->ssn_closed = true;
+    TSMutexUnlock(ssnData->disk_io_mutex);
+
+    break;
+  }
+  default:
+    break;
+  }
+  TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+  return TS_SUCCESS;
+}
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/session_data.h b/plugins/experimental/traffic_dump/session_data.h
new file mode 100644
index 0000000..0094e72
--- /dev/null
+++ b/plugins/experimental/traffic_dump/session_data.h
@@ -0,0 +1,182 @@
+/** @session_handler.h
+  Traffic Dump session handling encapsulation.
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#pragma once
+
+#include <atomic>
+#include <cstdlib>
+#include <string_view>
+
+#include "ts/ts.h"
+#include "tscore/ts_file.h"
+
+namespace traffic_dump
+{
+/** The information associated with an individual session.
+ *
+ * This class is responsible for containing the members associated with a
+ * particular session and defines the session handler callback.
+ */
+class SessionData
+{
+public:
+  /// By default, Traffic Dump logs will go into a directory called "dump".
+  static char const constexpr *const default_log_directory = "dump";
+  /// By default, 1 out of 1000 sessions will be dumped.
+  static constexpr int64_t default_sample_pool_size = 1000;
+  /// By default, logging will stop after 10 MB have been dumped.
+  static constexpr int64_t default_max_disk_usage = 10 * 1000 * 1000;
+
+private:
+  //
+  // Instance Variables
+  //
+
+  /// Log file descriptor for this session's dump file.
+  int log_fd = -1;
+  /// The count of the currently outstanding AIO operations.
+  int aio_count = 0;
+  /// The offset of the last point written to so for in the dump file for this
+  /// session.
+  int64_t write_offset = 0;
+  /// Whether this session has been closed.
+  bool ssn_closed = false;
+  /// The filename for this session's dump file.
+  ts::file::path log_name;
+  /// Whether the first transaction in this session has been written.
+  bool has_written_first_transaction = false;
+
+  TSCont aio_cont       = nullptr; /// AIO continuation callback
+  TSCont txn_cont       = nullptr; /// Transaction continuation callback
+  TSMutex disk_io_mutex = nullptr; /// AIO mutex
+
+  //
+  // Static Variables
+  //
+
+  // The index to be used for the TS API for storing this SessionData on a
+  // per-session basis.
+  static int session_arg_index;
+
+  /// The rate at which to dump sessions. Every one out of sample_pool_size will
+  /// be dumped.
+  static std::atomic<int64_t> sample_pool_size;
+  /// The maximum space logs should take up before stopping the dumping of new
+  /// sessions.
+  static std::atomic<int64_t> max_disk_usage;
+  /// The amount of bytes currently written to dump files.
+  static std::atomic<int64_t> disk_usage;
+
+  /// The directory into which to put the dump files.
+  static ts::file::path log_directory;
+
+  /// Only sessions with this SNI will be dumped (if set).
+  static std::string sni_filter;
+
+  /// The running counter of all sessions dumped by traffic_dump.
+  static uint64_t session_counter;
+
+public:
+  SessionData();
+  ~SessionData();
+
+  /** The getter for the session_arg_index value. */
+  static int get_session_arg_index();
+
+  /** Initialize the cross-session values of managing sessions.
+   *
+   * @return True if initialization is successful, false otherwise.
+   */
+  static bool init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size);
+  static bool init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size, std::string_view sni_filter);
+
+  /** Set the sample_pool_size to a new value.
+   *
+   * @param[in] new_sample_size The new value to set for sample_pool_size.
+   */
+  static void set_sample_pool_size(int64_t new_sample_size);
+
+  /** Reset the disk usage counter to 0. */
+  static void reset_disk_usage();
+
+  /** Set the max_disk_usage to a new value.
+   *
+   * @param[in] new_max_disk_usage The new value to set for max_disk_usage.
+   */
+  static void set_max_disk_usage(int64_t new_max_disk_usage);
+
+#if 0
+  // TODO: This will eventually be used by TransactionData to dump
+  // the server protocol description in the "server-response" node,
+  // but the TS API does not yet support this.
+
+  /** Get the JSON string that describes the server session stack.
+   *
+   * @param[in] ssnp The reference to the server session.
+   *
+   * @return A JSON description of the server protocol stack.
+   */
+  static std::string get_server_protocol_description(TSHttpSsn ssnp);
+#endif
+
+  /** Write the string to the session's dump file.
+   *
+   * @param[in] content The content to write to the file.
+   *
+   * @return TS_SUCCESS if the write is successfully scheduled with the AIO
+   * system, TS_ERROR otherwise.
+   */
+  int write_to_disk(std::string_view content);
+
+  /** Write the transaction to the session's dump file.
+   *
+   * @param[in] content The transaction content to write to the file.
+   *
+   * @return TS_SUCCESS if the write is successfully scheduled with the AIO
+   * system, TS_ERROR otherwise.
+   */
+  int write_transaction_to_disk(std::string_view content);
+
+private:
+  /** Write the string to the session's dump file.
+   *
+   * This assumes that the caller acquired the required AIO lock.
+   *
+   * @param[in] content The content to write to the file.
+   *
+   * @return TS_SUCCESS if the write is successfully scheduled with the AIO
+   * system, TS_ERROR otherwise.
+   */
+  int write_to_disk_no_lock(std::string_view content);
+
+  /** Get the JSON string that describes the client session stack.
+   *
+   * @param[in] ssnp The reference to the client session.
+   *
+   * @return A JSON description of the client protocol stack.
+   */
+  static std::string get_client_protocol_description(TSHttpSsn ssnp);
+
+  /** The handler callback for when async IO is done. Used for cleanup. */
+  static int session_aio_handler(TSCont contp, TSEvent event, void *edata);
+
+  /** The handler callback for session events. */
+  static int global_session_handler(TSCont contp, TSEvent event, void *edata);
+};
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/traffic_dump.cc b/plugins/experimental/traffic_dump/traffic_dump.cc
index f37bb7b..271b3b8 100644
--- a/plugins/experimental/traffic_dump/traffic_dump.cc
+++ b/plugins/experimental/traffic_dump/traffic_dump.cc
@@ -18,867 +18,71 @@
   limitations under the License.
  */
 
-#include <cinttypes>
-
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
 #include <getopt.h>
-#include <unistd.h>
-
-#include <sys/types.h>
-#include <fcntl.h>
-#include <cerrno>
-#include <arpa/inet.h>
-#include <netinet/in.h>
-#include <openssl/ssl.h>
-
-#include <algorithm>
-#include <sstream>
-#include <iomanip>
-#include <chrono>
-#include <atomic>
-#include <string>
-#include <string_view>
-#include <unordered_set>
-
-#include "tscore/ts_file.h"
-#include "tscpp/util/TextView.h"
-#include "ts/ts.h"
-
-namespace
-{
-const char *PLUGIN_NAME   = "traffic_dump";
-const std::string closing = "]}]}";
-uint64_t session_counter  = 0;
-
-std::string defaut_sensitive_field_value;
-
-// A case-insensitive comparitor used for comparing HTTP field names.
-struct InsensitiveCompare {
-  bool
-  operator()(std::string_view a, std::string_view b) const
-  {
-    return strcasecmp(a, b) == 0;
-  }
-};
-
-struct StringHashByLower {
-public:
-  size_t
-  operator()(const std::string &str) const
-  {
-    std::string lower;
-    std::transform(str.begin(), str.end(), lower.begin(), [](unsigned char c) -> unsigned char { return std::tolower(c); });
-    return std::hash<std::string>()(lower);
-  }
-};
-
-/// Fields considered sensitive because they may contain user-private
-/// information. These fields are replaced with auto-generated generic content by
-/// default. To override this behavior, the user should specify their own fields
-/// they consider sensitive with --sensitive-fields.
-///
-/// While these are specified with case, they are matched case-insensitively.
-std::unordered_set<std::string, StringHashByLower, InsensitiveCompare> default_sensitive_fields = {
-  "Set-Cookie",
-  "Cookie",
-};
-
-/// The set of fields, default and user-specified, that are sensitive and whose
-/// values will be replaced with auto-generated generic content.
-std::unordered_set<std::string, StringHashByLower, InsensitiveCompare> sensitive_fields;
-
-ts::file::path log_path{"dump"};               // default log directory
-std::string sni_filter;                        // The SNI requested for filtering against.
-int s_arg_idx = 0;                             // Session Arg Index to pass on session data
-std::atomic<int64_t> sample_pool_size(1000);   // Sampling ratio
-std::atomic<int64_t> max_disk_usage(10000000); //< Max disk space for logs (approximate)
-std::atomic<int64_t> disk_usage(0);            //< Actual disk usage
-// handler declaration
-int session_aio_handler(TSCont contp, TSEvent event, void *edata);
-int session_txn_handler(TSCont contp, TSEvent event, void *edata);
-
-/// Custom structure for per session data
-struct SsnData {
-  int log_fd           = -1;    //< Log file descriptor
-  int aio_count        = 0;     //< Active AIO counts
-  int64_t write_offset = 0;     //< AIO write offset
-  bool first           = true;  //< First Transaction
-  bool ssn_closed      = false; //< Session closed flag
-  ts::file::path log_name;      //< Log file path
-
-  TSCont aio_cont       = nullptr; //< AIO callback
-  TSCont txn_cont       = nullptr; //< Transaction callback
-  TSMutex disk_io_mutex = nullptr; //< AIO mutex
-
-  SsnData()
-  {
-    disk_io_mutex = TSMutexCreate();
-    aio_cont      = TSContCreate(session_aio_handler, TSMutexCreate());
-    txn_cont      = TSContCreate(session_txn_handler, nullptr);
-  }
-
-  ~SsnData()
-  {
-    if (disk_io_mutex) {
-      TSMutexDestroy(disk_io_mutex);
-    }
-    if (aio_cont) {
-      TSContDestroy(aio_cont);
-    }
-    if (txn_cont) {
-      TSContDestroy(txn_cont);
-    }
-  }
-
-  /// write_to_disk(): Takes a string object and writes to file via AIO
-  int
-  write_to_disk(const std::string &body)
-  {
-    TSMutexLock(disk_io_mutex);
-    char *pBuf = nullptr;
-    // Allocate a buffer for aio writing
-    if ((pBuf = static_cast<char *>(TSmalloc(sizeof(char) * body.size())))) {
-      memcpy(pBuf, body.c_str(), body.size());
-      if (TS_SUCCESS == TSAIOWrite(log_fd, write_offset, pBuf, body.size(), aio_cont)) {
-        // Update offset within file and aio events count
-        write_offset += body.size();
-        aio_count += 1;
-
-        TSMutexUnlock(disk_io_mutex);
-        return TS_SUCCESS;
-      }
-      TSfree(pBuf);
-    }
-    TSMutexUnlock(disk_io_mutex);
-    return TS_ERROR;
-  }
-};
-
-/// Local helper functions about json formatting
-/// min_write(): Inline function for repeating code
-inline void
-min_write(const char *buf, int64_t &prevIdx, int64_t &idx, std::ostream &jsonfile)
-{
-  if (prevIdx < idx) {
-    jsonfile.write(buf + prevIdx, idx - prevIdx);
-  }
-  prevIdx = idx + 1;
-}
-
-/// esc_json_out(): Escape characters in a buffer and output to ofstream object
-///                 in a way to minimize ofstream operations
-int
-esc_json_out(const char *buf, int64_t len, std::ostream &jsonfile)
-{
-  if (buf == nullptr) {
-    return 0;
-  }
-  int64_t idx, prevIdx = 0;
-  for (idx = 0; idx < len; idx++) {
-    char c = *(buf + idx);
-    switch (c) {
-    case '"':
-    case '\\': {
-      min_write(buf, prevIdx, idx, jsonfile);
-      jsonfile << "\\" << c;
-      break;
-    }
-    case '\b': {
-      min_write(buf, prevIdx, idx, jsonfile);
-      jsonfile << "\\b";
-      break;
-    }
-    case '\f': {
-      min_write(buf, prevIdx, idx, jsonfile);
-      jsonfile << "\\f";
-      break;
-    }
-    case '\n': {
-      min_write(buf, prevIdx, idx, jsonfile);
-      jsonfile << "\\n";
-      break;
-    }
-    case '\r': {
-      min_write(buf, prevIdx, idx, jsonfile);
-      jsonfile << "\\r";
-      break;
-    }
-    case '\t': {
-      min_write(buf, prevIdx, idx, jsonfile);
-      jsonfile << "\\t";
-      break;
-    }
-    default: {
-      if ('\x00' <= c && c <= '\x1f') {
-        min_write(buf, prevIdx, idx, jsonfile);
-        jsonfile << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(c);
-      }
-      break;
-    }
-    }
-  }
-  min_write(buf, prevIdx, idx, jsonfile);
-
-  return len;
-}
-
-/// escape_json(): escape chars in a string and returns json string
-std::string
-escape_json(std::string_view s)
-{
-  std::ostringstream o;
-  esc_json_out(s.data(), s.length(), o);
-  return o.str();
-}
-std::string
-escape_json(const char *buf, int64_t size)
-{
-  std::ostringstream o;
-  esc_json_out(buf, size, o);
-  return o.str();
-}
-
-inline std::string
-json_entry(std::string const &name, const char *value, int64_t size)
-{
-  return "\"" + escape_json(name) + "\":\"" + escape_json(value, size) + "\"";
-}
-
-/// json_entry_array(): Formats to array-style entry i.e. ["field","value"]
-inline std::string
-json_entry_array(std::string_view name, std::string_view value)
-{
-  return "[\"" + escape_json(name) + "\", \"" + escape_json(value) + "\"]";
-}
-
-/** Remove the scheme prefix from the url.
- *
- * @return The view without the scheme prefix.
- */
-std::string_view
-remove_scheme_prefix(std::string_view url)
-{
-  const auto scheme_separator = url.find("://");
-  if (scheme_separator == std::string::npos) {
-    return url;
-  }
-  url.remove_prefix(scheme_separator + 3);
-  return url;
-}
-
-/// Write the content node.
-//
-/// "content"
-///    "encoding"
-///    "size"
-std::string
-write_content_node(int64_t num_body_bytes)
-{
-  return std::string(R"(,"content":{"encoding":"plain","size":)" + std::to_string(num_body_bytes) + '}');
-}
-
-/** Initialize the generic sensitive field to be dumped. This is used instead
- * of the sensitive field values seen on the wire.
- */
-void
-initialize_default_sensitive_field()
-{
-  // 128 KB is the maximum size supported for all headers, so this size should
-  // be plenty large for our needs.
-  constexpr size_t default_field_size = 128 * 1024;
-  defaut_sensitive_field_value.resize(default_field_size);
-
-  char *field_buffer = defaut_sensitive_field_value.data();
-  for (auto i = 0u; i < default_field_size; i += 8) {
-    sprintf(field_buffer, "%07x ", i / 8);
-    field_buffer += 8;
-  }
-}
-
-/** Inspect the field to see whether it is sensitive and return a generic value
- * of equal size to the original if it is.
- *
- * @param[in] name The field name to inspect.
- * @param[in] original_value The field value to inspect.
- *
- * @return The value traffic_dump should dump for the given field.
- */
-std::string_view
-replace_sensitive_fields(std::string_view name, std::string_view original_value)
-{
-  auto search = sensitive_fields.find(std::string(name));
-  if (search == sensitive_fields.end()) {
-    return original_value;
-  }
-  auto new_value_size = original_value.size();
-  if (original_value.size() > defaut_sensitive_field_value.size()) {
-    new_value_size = defaut_sensitive_field_value.size();
-    TSError("[%s] Encountered a sensitive field value larger than our default "
-            "field size. Default size: %zu, incoming field size: %zu",
-            PLUGIN_NAME, defaut_sensitive_field_value.size(), original_value.size());
-  }
-  return std::string_view{defaut_sensitive_field_value.data(), new_value_size};
-}
-
-/// Read the txn information from TSMBuffer and write the header information.
-/// This function does not write the content node.
-std::string
-write_message_node_no_content(TSMBuffer &buffer, TSMLoc &hdr_loc)
-{
-  std::string result = "{";
-  int len            = 0;
-  const char *cp     = nullptr;
-  TSMLoc url_loc     = nullptr;
-
-  // Log scheme+method+request-target or status+reason based on header type
-  if (TSHttpHdrTypeGet(buffer, hdr_loc) == TS_HTTP_TYPE_REQUEST) {
-    // 1. "version"
-    int version = TSHttpHdrVersionGet(buffer, hdr_loc);
-    result += R"("version":")" + std::to_string(TS_HTTP_MAJOR(version)) + "." + std::to_string(TS_HTTP_MINOR(version)) + '"';
-
-    TSAssert(TS_SUCCESS == TSHttpHdrUrlGet(buffer, hdr_loc, &url_loc));
-    // 2. "scheme":
-    cp = TSUrlSchemeGet(buffer, url_loc, &len);
-    TSDebug(PLUGIN_NAME, "write_message_node(): found scheme %.*s ", len, cp);
-    result += "," + json_entry("scheme", cp, len);
-
-    // 3. "method":(string)
-    cp = TSHttpHdrMethodGet(buffer, hdr_loc, &len);
-    TSDebug(PLUGIN_NAME, "write_message_node(): found method %.*s ", len, cp);
-    result += "," + json_entry("method", cp, len);
-
-    // 4. "url"
-    cp = TSUrlHostGet(buffer, url_loc, &len);
-    std::string_view host{cp, static_cast<size_t>(len)};
-
-    char *url = TSUrlStringGet(buffer, url_loc, &len);
-    std::string_view url_string{url, static_cast<size_t>(len)};
-
-    if (host.empty()) {
-      // TSUrlStringGet will add the scheme to the URL, even if the request
-      // target doesn't contain it. However, we cannot just always remove the
-      // scheme because the original request target may include it. We assume
-      // here that a URL with a scheme but not a host is artificial and thus
-      // we remove it.
-      url_string = remove_scheme_prefix(url_string);
-    }
-
-    TSDebug(PLUGIN_NAME, "write_message_node(): found host target %.*s", static_cast<int>(url_string.size()), url_string.data());
-    result += "," + json_entry("url", url_string.data(), url_string.size());
-    TSfree(url);
-    TSHandleMLocRelease(buffer, hdr_loc, url_loc);
-  } else {
-    // 1. "status":(string)
-    result += R"("status":)" + std::to_string(TSHttpHdrStatusGet(buffer, hdr_loc));
-    // 2. "reason":(string)
-    cp = TSHttpHdrReasonGet(buffer, hdr_loc, &len);
-    result += "," + json_entry("reason", cp, len);
-    // 3. "encoding"
-  }
-
-  // "headers": [[name(string), value(string)]]
-  result += R"(,"headers":{"encoding":"esc_json", "fields": [)";
-  TSMLoc field_loc = TSMimeHdrFieldGet(buffer, hdr_loc, 0);
-  while (field_loc) {
-    TSMLoc next_field_loc;
-    const char *name  = nullptr;
-    const char *value = nullptr;
-    int name_len = 0, value_len = 0;
-    // Append to "fields" list if valid value exists
-    if ((name = TSMimeHdrFieldNameGet(buffer, hdr_loc, field_loc, &name_len)) && name_len) {
-      std::string_view name_view{name, static_cast<size_t>(name_len)};
-      value = TSMimeHdrFieldValueStringGet(buffer, hdr_loc, field_loc, -1, &value_len);
-      std::string_view value_view{value, static_cast<size_t>(value_len)};
-      std::string_view new_value = replace_sensitive_fields(name_view, value_view);
-      result += json_entry_array(name_view, new_value);
-    }
-
-    next_field_loc = TSMimeHdrFieldNext(buffer, hdr_loc, field_loc);
-    TSHandleMLocRelease(buffer, hdr_loc, field_loc);
-    if ((field_loc = next_field_loc) != nullptr) {
-      result += ",";
-    }
-  }
-  return result += "]}";
-}
-
-/// Read the txn information from TSMBuffer and write the header information including
-/// the content node describing the body characteristics.
-std::string
-write_message_node(TSMBuffer &buffer, TSMLoc &hdr_loc, int64_t num_body_bytes)
-{
-  std::string result = write_message_node_no_content(buffer, hdr_loc);
-  result += write_content_node(num_body_bytes);
-  return result + "}";
-}
-
-// Per session AIO handler: update AIO counts and clean up
-int
-session_aio_handler(TSCont contp, TSEvent event, void *edata)
-{
-  switch (event) {
-  case TS_EVENT_AIO_DONE: {
-    TSAIOCallback cb = static_cast<TSAIOCallback>(edata);
-    SsnData *ssnData = static_cast<SsnData *>(TSContDataGet(contp));
-    if (!ssnData) {
-      TSDebug(PLUGIN_NAME, "session_aio_handler(): No valid ssnData. Abort.");
-      return TS_ERROR;
-    }
-    char *buf = TSAIOBufGet(cb);
-    TSMutexLock(ssnData->disk_io_mutex);
-
-    // Free the allocated buffer and update aio_count
-    if (buf) {
-      TSfree(buf);
-      if (--ssnData->aio_count == 0 && ssnData->ssn_closed) {
-        // check for ssn close, if closed, do clean up
-        TSContDataSet(contp, nullptr);
-        close(ssnData->log_fd);
-        TSMutexUnlock(ssnData->disk_io_mutex);
-        std::error_code ec;
-        ts::file::file_status st = ts::file::status(ssnData->log_name, ec);
-        if (!ec) {
-          disk_usage += ts::file::file_size(st);
-          TSDebug(PLUGIN_NAME, "Finish a session with log file of %" PRIuMAX "bytes", ts::file::file_size(st));
-        }
-        delete ssnData;
-        return TS_SUCCESS;
-      }
-    }
-    TSMutexUnlock(ssnData->disk_io_mutex);
-    return TS_SUCCESS;
-  }
-  default:
-    TSDebug(PLUGIN_NAME, "session_aio_handler(): unhandled events %d", event);
-    return TS_ERROR;
-  }
-  return TS_SUCCESS;
-}
-
-// Transaction handler: writes headers to the log file using AIO
-int
-session_txn_handler(TSCont contp, TSEvent event, void *edata)
-{
-  TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
-
-  // Retrieve SsnData
-  TSHttpSsn ssnp   = TSHttpTxnSsnGet(txnp);
-  SsnData *ssnData = static_cast<SsnData *>(TSUserArgGet(ssnp, s_arg_idx));
-
-  // If no valid ssnData, continue transaction as if nothing happened
-  if (!ssnData) {
-    TSDebug(PLUGIN_NAME, "session_txn_handler(): No ssnData found. Abort.");
-    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
-    return TS_SUCCESS;
-  }
-
-  std::string txn_info;
-  switch (event) {
-  case TS_EVENT_HTTP_TXN_START: {
-    // Get UUID
-    char uuid[TS_CRUUID_STRING_LEN + 1];
-    TSAssert(TS_SUCCESS == TSClientRequestUuidGet(txnp, uuid));
-    std::string_view uuid_view{uuid, strnlen(uuid, TS_CRUUID_STRING_LEN)};
-
-    // Generate per transaction json records
-    if (!ssnData->first) {
-      txn_info += ",";
-    }
-    ssnData->first = false;
-
-    // "uuid":(string)
-    txn_info += "{";
-    // "connection-time":(number)
-    TSHRTime start_time;
-    TSHttpTxnMilestoneGet(txnp, TS_MILESTONE_UA_BEGIN, &start_time);
-    txn_info += "\"connection-time\":" + std::to_string(start_time);
-
-    // The uuid is a header field for each message in the transaction. Use the
-    // "all" node to apply to each message.
-    std::string_view name = "uuid";
-    txn_info += ",\"all\":{\"headers\":{\"fields\":[" + json_entry_array(name, uuid_view);
-    txn_info += "]}}";
-    ssnData->write_to_disk(txn_info);
-    break;
-  }
-
-  case TS_EVENT_HTTP_READ_REQUEST_HDR: {
-    // This hook is registered globally, not at TS_EVENT_HTTP_SSN_START in
-    // global_ssn_handler(). As such, this handler will be called with every
-    // transaction. However, we know that we are dumping this transaction
-    // because there is a ssnData associated with it.
-
-    // We must grab the client request information before remap happens because
-    // the remap process modifies the request buffer.
-    TSMBuffer buffer;
-    TSMLoc hdr_loc;
-    if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
-      TSDebug(PLUGIN_NAME, "Found client request");
-      // We don't have an accurate view of the body size until TXN_CLOSE so we hold
-      // off on writing the content:size node until then.
-      txn_info += R"(,"client-request":)" + write_message_node_no_content(buffer, hdr_loc);
-      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
-      buffer = nullptr;
-    }
-    ssnData->write_to_disk(txn_info);
-    break;
-  }
-
-  case TS_EVENT_HTTP_TXN_CLOSE: {
-    // proxy-request/response headers
-    TSMBuffer buffer;
-    TSMLoc hdr_loc;
-    if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
-      txn_info += write_content_node(TSHttpTxnClientReqBodyBytesGet(txnp)) + "}";
-      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
-      buffer = nullptr;
-    }
-    if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &buffer, &hdr_loc)) {
-      TSDebug(PLUGIN_NAME, "Found proxy request");
-      txn_info += R"(,"proxy-request":)" + write_message_node(buffer, hdr_loc, TSHttpTxnServerReqBodyBytesGet(txnp));
-      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
-      buffer = nullptr;
-    }
-    if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &buffer, &hdr_loc)) {
-      TSDebug(PLUGIN_NAME, "Found server response");
-      txn_info += R"(,"server-response":)" + write_message_node(buffer, hdr_loc, TSHttpTxnServerRespBodyBytesGet(txnp));
-      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
-      buffer = nullptr;
-    }
-    if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &buffer, &hdr_loc)) {
-      TSDebug(PLUGIN_NAME, "Found proxy response");
-      txn_info += R"(,"proxy-response":)" + write_message_node(buffer, hdr_loc, TSHttpTxnClientRespBodyBytesGet(txnp));
-      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
-      buffer = nullptr;
-    }
-
-    txn_info += "}";
-    ssnData->write_to_disk(txn_info);
-    break;
-  }
-  default:
-    TSDebug(PLUGIN_NAME, "session_txn_handler(): Unhandled events %d", event);
-    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
-    return TS_ERROR;
-  }
-
-  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
-  return TS_SUCCESS;
-}
-
-/** Create a TLS characteristics node.
- *
- * This function encapsulates the logic common between the client-side and
- * server-side logic for populating the TLS node.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The node describing the TLS properties of this session.
- */
-std::string
-get_tls_description_helper(TSVConn ssn_vc)
-{
-  TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
-  SSL *ssl_obj             = (SSL *)ssl_conn;
-  if (ssl_obj == nullptr) {
-    return "";
-  }
-  std::ostringstream tls_description;
-  tls_description << R"("tls":{)";
-  const char *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
-  if (sni_ptr != nullptr) {
-    std::string_view sni{sni_ptr};
-    if (!sni.empty()) {
-      tls_description << R"("sni":")" << sni << R"(")";
-    }
-  }
-  tls_description << R"(,"verify_mode":")" << std::to_string(SSL_get_verify_mode(ssl_obj)) << R"(")";
-  tls_description << "}";
-  return tls_description.str();
-}
-
-/** Create a server-side TLS characteristics node.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The node describing the TLS properties of this session.
- */
-std::string
-get_server_tls_description(TSHttpSsn ssnp)
-{
-  TSVConn ssn_vc = TSHttpSsnServerVConnGet(ssnp);
-  return get_tls_description_helper(ssn_vc);
-}
-
-/** Create a client-side TLS characteristics node.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The node describing the TLS properties of this session.
- */
-std::string
-get_client_tls_description(TSHttpSsn ssnp)
-{
-  TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp);
-  return get_tls_description_helper(ssn_vc);
-}
-
-/// A named boolean for callers who pass the is_client parameter.
-constexpr bool IS_CLIENT = true;
-
-/** Create the nodes that describe the session's sub-HTTP protocols.
- *
- * This function encapsulates the logic common between the client-side and
- * server-side logic for describing the session's characteristics.
- *
- * This will create the string representing the "protocol" and "tls" nodes. The
- * "tls" node will only be present if the connection is over SSL/TLS.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The description of the protocol stack and certain TLS attributes.
- */
-std::string
-get_protocol_description_helper(TSHttpSsn ssnp, bool is_client)
-{
-  std::ostringstream protocol_description;
-  protocol_description << R"("protocol":[)";
-
-  const char *protocol[10];
-  int count = -1;
-  if (is_client) {
-    TSAssert(TS_SUCCESS == TSHttpSsnClientProtocolStackGet(ssnp, 10, protocol, &count));
-  } else {
-    // See the TODO below in the commented out defintion of get_server_protocol_description.
-    // TSAssert(TS_SUCCESS == TSHttpSsnServerProtocolStackGet(ssnp, 10, protocol, &count));
-  }
-  for (int i = 0; i < count; i++) {
-    if (i > 0) {
-      protocol_description << ",";
-    }
-    protocol_description << '"' << std::string(protocol[i]) << '"';
-  }
-
-  protocol_description << "]";
-  std::string tls_description;
-  if (is_client) {
-    tls_description = get_client_tls_description(ssnp);
-  } else {
-    tls_description = get_server_tls_description(ssnp);
-  }
-  if (!tls_description.empty()) {
-    protocol_description << "," << tls_description;
-  }
-  return protocol_description.str();
-}
 
-#if 0
-// TODO It will be important to add this eventually, but
-// TSHttpSsnServerProtocolStackGet is not defined yet. Once it (or some other
-// mechanism for getting the server side stack) is implemented, we will call
-// this as a part of writing the server-response node.
+#include "global_variables.h"
+#include "session_data.h"
+#include "transaction_data.h"
 
-/** Generate the nodes describing the server session.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The description of the protocol stack and certain TLS attributes.
- */
-std::string
-get_server_protocol_description(TSHttpSsn ssnp)
-{
-  return get_protocol_description_helper(ssnp, !IS_CLIENT);
-}
-#endif
-
-/** Generate the nodes describing the client session.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The description of the protocol stack and certain TLS attributes.
- */
-std::string
-get_client_protocol_description(TSHttpSsn ssnp)
+namespace traffic_dump
 {
-  return get_protocol_description_helper(ssnp, IS_CLIENT);
-}
-
-// Session handler for global hooks; Assign per-session data structure and log files
+/// Handle LIFECYCLE_MSG from traffic_ctl.
 static int
-global_ssn_handler(TSCont contp, TSEvent event, void *edata)
+global_message_handler(TSCont contp, TSEvent event, void *edata)
 {
-  TSHttpSsn ssnp = static_cast<TSHttpSsn>(edata);
-
   switch (event) {
-  // Also handles LIFECYCLE_MSG from traffic_ctl
   case TS_EVENT_LIFECYCLE_MSG: {
     TSPluginMsg *msg = static_cast<TSPluginMsg *>(edata);
-    // String view of plugin message prefix
     static constexpr std::string_view PLUGIN_PREFIX("traffic_dump."_sv);
 
     std::string_view tag(msg->tag, strlen(msg->tag));
-
     if (tag.substr(0, PLUGIN_PREFIX.size()) == PLUGIN_PREFIX) {
       tag.remove_prefix(PLUGIN_PREFIX.size());
       if (tag == "sample") {
-        sample_pool_size = static_cast<int64_t>(strtol(static_cast<const char *>(msg->data), nullptr, 0));
-        TSDebug(PLUGIN_NAME, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change sample size to %" PRId64 "bytes",
-                sample_pool_size.load());
+        const auto new_sample_size = static_cast<int64_t>(strtol(static_cast<char const *>(msg->data), nullptr, 0));
+        TSDebug(debug_tag, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change sample size to %" PRId64 "bytes", new_sample_size);
+        SessionData::set_sample_pool_size(new_sample_size);
       } else if (tag == "reset") {
-        disk_usage = 0;
-        TSDebug(PLUGIN_NAME, "TS_EVENT_LIFECYCLE_MSG: Received Msg to reset disk usage counter");
+        TSDebug(debug_tag, "TS_EVENT_LIFECYCLE_MSG: Received Msg to reset disk usage counter");
+        SessionData::reset_disk_usage();
       } else if (tag == "limit") {
-        max_disk_usage = static_cast<int64_t>(strtol(static_cast<const char *>(msg->data), nullptr, 0));
-        TSDebug(PLUGIN_NAME, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change max disk usage to %" PRId64 "bytes",
-                max_disk_usage.load());
+        const auto new_max_disk_usage = static_cast<int64_t>(strtol(static_cast<char const *>(msg->data), nullptr, 0));
+        TSDebug(debug_tag, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change max disk usage to %" PRId64 "bytes", new_max_disk_usage);
+        SessionData::set_max_disk_usage(new_max_disk_usage);
       }
     }
     return TS_SUCCESS;
   }
-  case TS_EVENT_HTTP_SSN_START: {
-    // Grab session id for logging against a global value rather than the local
-    // session_counter.
-    int64_t id = TSHttpSsnIdGet(ssnp);
-
-    // If the user has asked for SNI filtering, filter on that first because
-    // any sampling will apply just to that subset of connections that match
-    // that SNI.
-    if (!sni_filter.empty()) {
-      TSVConn ssn_vc           = TSHttpSsnClientVConnGet(ssnp);
-      TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
-      SSL *ssl_obj             = (SSL *)ssl_conn;
-      if (ssl_obj == nullptr) {
-        TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore non-HTTPS session %" PRId64 "...", id);
-        break;
-      }
-      const char *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
-      if (sni_ptr == nullptr) {
-        TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore HTTPS session with non-existent SNI.");
-        break;
-      } else {
-        const std::string_view sni{sni_ptr};
-        if (sni != sni_filter) {
-          TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore HTTPS session with non-filtered SNI: %s", sni_ptr);
-          break;
-        }
-      }
-    }
-    const auto this_session_count = session_counter++;
-    if (this_session_count % sample_pool_size != 0) {
-      TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore session %" PRId64 "...", id);
-      break;
-    } else if (disk_usage >= max_disk_usage) {
-      TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore session %" PRId64 "due to disk usage %" PRId64 "bytes", id,
-              disk_usage.load());
-      break;
-    }
-    // Beginning of a new session
-    /// Get epoch time
-    auto start = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch());
-
-    // Create new per session data
-    SsnData *ssnData = new SsnData;
-    TSUserArgSet(ssnp, s_arg_idx, ssnData);
-
-    TSContDataSet(ssnData->aio_cont, ssnData);
-
-    // "protocol":(string),"tls":(string)
-    // The "tls" node will only be present if the session is over SSL/TLS.
-    std::string protocol_description = get_client_protocol_description(ssnp);
-
-    std::string beginning = R"({"meta":{"version":"1.0"},"sessions":[{)" + protocol_description + R"(,"connection-time":)" +
-                            std::to_string(start.count()) + R"(,"transactions":[)";
-
-    // Use the session count's hex string as the filename.
-    std::stringstream stream;
-    stream << std::setw(16) << std::setfill('0') << std::hex << this_session_count;
-    std::string session_hex_name = stream.str();
-
-    // Use client ip as sub directory name
-    char client_str[INET6_ADDRSTRLEN];
-    const sockaddr *client_ip = TSHttpSsnClientAddrGet(ssnp);
-    if (AF_INET == client_ip->sa_family) {
-      inet_ntop(AF_INET, &(reinterpret_cast<const sockaddr_in *>(client_ip)->sin_addr), client_str, INET_ADDRSTRLEN);
-    } else if (AF_INET6 == client_ip->sa_family) {
-      inet_ntop(AF_INET6, &(reinterpret_cast<const sockaddr_in6 *>(client_ip)->sin6_addr), client_str, INET6_ADDRSTRLEN);
-    } else {
-      TSDebug(PLUGIN_NAME, "global_ssn_handler(): Unknown address family.");
-      snprintf(client_str, INET6_ADDRSTRLEN, "unknown");
-    }
-
-    // Initialize AIO file
-    TSMutexLock(ssnData->disk_io_mutex);
-    if (ssnData->log_fd < 0) {
-      ts::file::path log_p = log_path / ts::file::path(std::string(client_str, 3));
-      ts::file::path log_f = log_p / ts::file::path(session_hex_name);
-
-      // Create subdir if not existing
-      std::error_code ec;
-      ts::file::status(log_p, ec);
-      if (ec && mkdir(log_p.c_str(), 0755) == -1) {
-        TSDebug(PLUGIN_NAME, "global_ssn_handler(): Failed to create dir %s", log_p.c_str());
-        TSError("[%s] Failed to create dir %s", PLUGIN_NAME, log_p.c_str());
-      }
-
-      // Try to open log files for AIO
-      ssnData->log_fd = open(log_f.c_str(), O_RDWR | O_CREAT, S_IRWXU);
-      if (ssnData->log_fd < 0) {
-        TSMutexUnlock(ssnData->disk_io_mutex);
-        TSDebug(PLUGIN_NAME, "global_ssn_handler(): Failed to open log files %s. Abort.", log_f.c_str());
-        TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
-        return TS_EVENT_HTTP_CONTINUE;
-      }
-      ssnData->log_name = log_f;
-      // Write log file beginning to disk
-      ssnData->write_to_disk(beginning);
-    }
-    TSMutexUnlock(ssnData->disk_io_mutex);
-
-    TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_START_HOOK, ssnData->txn_cont);
-    TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_CLOSE_HOOK, ssnData->txn_cont);
-    break;
-  }
-  case TS_EVENT_HTTP_SSN_CLOSE: {
-    // Write session and close the log file.
-    int64_t id = TSHttpSsnIdGet(ssnp);
-    TSDebug(PLUGIN_NAME, "global_ssn_handler(): Closing session %" PRId64 "...", id);
-    // Retrieve SsnData
-    SsnData *ssnData = static_cast<SsnData *>(TSUserArgGet(ssnp, s_arg_idx));
-    // If no valid ssnData, continue transaction as if nothing happened
-    if (!ssnData) {
-      TSDebug(PLUGIN_NAME, "global_ssn_handler(): [TS_EVENT_HTTP_SSN_CLOSE] No ssnData found. Abort.");
-      TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
-      return TS_SUCCESS;
-    }
-    ssnData->write_to_disk(closing);
-    TSMutexLock(ssnData->disk_io_mutex);
-    ssnData->ssn_closed = true;
-    TSMutexUnlock(ssnData->disk_io_mutex);
-
-    break;
-  }
   default:
-    break;
+    TSDebug(debug_tag, "session_aio_handler(): unhandled events %d", event);
+    return TS_ERROR;
   }
-  TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
-  return TS_SUCCESS;
 }
 
-} // End of anonymous namespace
+} // namespace traffic_dump
 
 void
-TSPluginInit(int argc, const char *argv[])
+TSPluginInit(int argc, char const *argv[])
 {
-  TSDebug(PLUGIN_NAME, "initializing plugin");
+  TSDebug(traffic_dump::debug_tag, "initializing plugin");
   TSPluginRegistrationInfo info;
 
   info.plugin_name   = "traffic_dump";
   info.vendor_name   = "Apache Software Foundation";
   info.support_email = "dev@trafficserver.apache.org";
 
+  if (TS_SUCCESS != TSPluginRegister(&info)) {
+    TSError("[%s] Unable to initialize plugin (disabled). Failed to register plugin.", traffic_dump::debug_tag);
+    return;
+  }
+
   bool sensitive_fields_were_specified = false;
+  traffic_dump::sensitive_fields_t user_specified_fields;
+  ts::file::path log_dir{traffic_dump::SessionData::default_log_directory};
+  int64_t sample_pool_size = traffic_dump::SessionData::default_sample_pool_size;
+  int64_t max_disk_usage   = traffic_dump::SessionData::default_max_disk_usage;
+  std::string sni_filter;
+
   /// Commandline options
   static const struct option longopts[] = {
     {"logdir", required_argument, nullptr, 'l'},     {"sample", required_argument, nullptr, 's'},
@@ -905,18 +109,17 @@ TSPluginInit(int argc, const char *argv[])
         if (filter_field.empty()) {
           continue;
         }
-        sensitive_fields.emplace(filter_field);
+        user_specified_fields.emplace(filter_field);
       }
       break;
     }
     case 'n': {
       // --sni-filter is used to filter sessions based upon an SNI.
       sni_filter = std::string(optarg);
-      TSDebug(PLUGIN_NAME, "Filtering to only dump connections with SNI: %s", sni_filter.c_str());
       break;
     }
     case 'l': {
-      log_path = ts::file::path{optarg};
+      log_dir = ts::file::path{optarg};
       break;
     }
     case 's': {
@@ -931,57 +134,41 @@ TSPluginInit(int argc, const char *argv[])
       break;
 
     default:
-      TSDebug(PLUGIN_NAME, "Unexpected options.");
-      TSError("[%s] Unexpected options error.", PLUGIN_NAME);
+      TSDebug(traffic_dump::debug_tag, "Unexpected options.");
+      TSError("[%s] Unexpected options error.", traffic_dump::debug_tag);
       return;
     }
   }
-
-  if (!sensitive_fields_were_specified) {
-    // The user did not provide their own list of sensitive fields. Use the
-    // default.
-    sensitive_fields.merge(default_sensitive_fields);
+  if (!log_dir.is_absolute()) {
+    log_dir = ts::file::path(TSInstallDirGet()) / log_dir;
   }
-
-  std::string sensitive_fields_string;
-  bool is_first = true;
-  for (const auto &field : sensitive_fields) {
-    if (!is_first) {
-      sensitive_fields_string += ", ";
+  if (sni_filter.empty()) {
+    if (!traffic_dump::SessionData::init(log_dir.view(), max_disk_usage, sample_pool_size)) {
+      TSError("[%s] Failed to initialize session state.", traffic_dump::debug_tag);
+      return;
+    }
+  } else {
+    if (!traffic_dump::SessionData::init(log_dir.view(), max_disk_usage, sample_pool_size, sni_filter)) {
+      TSError("[%s] Failed to initialize session state with an SNI filter.", traffic_dump::debug_tag);
+      return;
     }
-    is_first = false;
-    sensitive_fields_string += field;
-  }
-  TSDebug(PLUGIN_NAME, "Sensitive fields for which generic values will be dumped: %s", sensitive_fields_string.c_str());
-
-  // Make absolute path if not
-  if (!log_path.is_absolute()) {
-    log_path = ts::file::path(TSInstallDirGet()) / log_path;
   }
-  TSDebug(PLUGIN_NAME, "Initialized with log directory: %s", log_path.c_str());
 
-  if (TS_SUCCESS != TSPluginRegister(&info)) {
-    TSError("[%s] Unable to initialize plugin (disabled). Failed to register plugin.", PLUGIN_NAME);
-  } else if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_SSN, PLUGIN_NAME, "Track log related data", &s_arg_idx)) {
-    TSError("[%s] Unable to initialize plugin (disabled). Failed to reserve ssn arg.", PLUGIN_NAME);
+  if (sensitive_fields_were_specified) {
+    if (!traffic_dump::TransactionData::init(std::move(user_specified_fields))) {
+      TSError("[%s] Failed to initialize transaction state with user-specified fields.", traffic_dump::debug_tag);
+      return;
+    }
   } else {
-    initialize_default_sensitive_field();
-
-    /// Add global hooks
-    TSCont ssncont = TSContCreate(global_ssn_handler, nullptr);
-    TSHttpHookAdd(TS_HTTP_SSN_START_HOOK, ssncont);
-    TSHttpHookAdd(TS_HTTP_SSN_CLOSE_HOOK, ssncont);
-
-    // Register the collecting of client-request headers at the global level so
-    // we can process requests before other plugins. (Global hooks are
-    // processed before session and transaction ones.)
-    TSCont txn_cont = TSContCreate(session_txn_handler, nullptr);
-    TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, txn_cont);
-
-    TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, ssncont);
-    TSDebug(PLUGIN_NAME, "Initialized with sample pool size %" PRId64 " bytes and disk limit %" PRId64 " bytes",
-            sample_pool_size.load(), max_disk_usage.load());
+    // The user did not provide their own list of sensitive fields. Use the
+    // default.
+    if (!traffic_dump::TransactionData::init()) {
+      TSError("[%s] Failed to initialize transaction state.", traffic_dump::debug_tag);
+      return;
+    }
   }
 
+  TSCont message_continuation = TSContCreate(traffic_dump::global_message_handler, nullptr);
+  TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, message_continuation);
   return;
 }
diff --git a/plugins/experimental/traffic_dump/transaction_data.cc b/plugins/experimental/traffic_dump/transaction_data.cc
new file mode 100644
index 0000000..4951f2f
--- /dev/null
+++ b/plugins/experimental/traffic_dump/transaction_data.cc
@@ -0,0 +1,358 @@
+/** @txn_data.cc
+  Implementation of Traffic Dump transaction data.
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "transaction_data.h"
+#include "global_variables.h"
+#include "json_utils.h"
+#include "session_data.h"
+
+namespace traffic_dump
+{
+int TransactionData::transaction_arg_index = 0;
+sensitive_fields_t TransactionData::sensitive_fields;
+
+std::string defaut_sensitive_field_value;
+
+/// Fields considered sensitive because they may contain user-private
+/// information. These fields are replaced with auto-generated generic content by
+/// default. To override this behavior, the user should specify their own fields
+/// they consider sensitive with --sensitive-fields.
+///
+/// While these are specified with case, they are matched case-insensitively.
+sensitive_fields_t default_sensitive_fields = {
+  "Set-Cookie",
+  "Cookie",
+};
+
+void
+TransactionData::initialize_default_sensitive_field()
+{
+  // 128 KB is the maximum size supported for all headers, so this size should
+  // be plenty large for our needs.
+  constexpr size_t default_field_size = 128 * 1024;
+  defaut_sensitive_field_value.resize(default_field_size);
+
+  char *field_buffer = defaut_sensitive_field_value.data();
+  for (auto i = 0u; i < default_field_size; i += 8) {
+    sprintf(field_buffer, "%07x ", i / 8);
+    field_buffer += 8;
+  }
+}
+
+/// The set of fields, default and user-specified, that are sensitive and whose
+/// values will be replaced with auto-generated generic content.
+sensitive_fields_t sensitive_fields;
+
+std::string
+TransactionData::get_sensitive_field_description()
+{
+  std::string sensitive_fields_string;
+  bool is_first = true;
+  for (const auto &field : sensitive_fields) {
+    if (!is_first) {
+      sensitive_fields_string += ", ";
+    }
+    is_first = false;
+    sensitive_fields_string += field;
+  }
+  return sensitive_fields_string;
+}
+
+bool
+TransactionData::init(sensitive_fields_t &&new_fields)
+{
+  sensitive_fields = std::move(new_fields);
+  return init_helper();
+}
+
+bool
+TransactionData::init()
+{
+  sensitive_fields = default_sensitive_fields;
+  return init_helper();
+}
+
+std::string_view
+TransactionData::replace_sensitive_fields(std::string_view name, std::string_view original_value)
+{
+  auto search = sensitive_fields.find(std::string(name));
+  if (search == sensitive_fields.end()) {
+    return original_value;
+  }
+  auto new_value_size = original_value.size();
+  if (original_value.size() > defaut_sensitive_field_value.size()) {
+    new_value_size = defaut_sensitive_field_value.size();
+    TSError("[%s] Encountered a sensitive field value larger than our default "
+            "field size. Default size: %zu, incoming field size: %zu",
+            debug_tag, defaut_sensitive_field_value.size(), original_value.size());
+  }
+  return std::string_view{defaut_sensitive_field_value.data(), new_value_size};
+}
+
+std::string
+TransactionData::write_content_node(int64_t num_body_bytes)
+{
+  return std::string(R"(,"content":{"encoding":"plain","size":)" + std::to_string(num_body_bytes) + '}');
+}
+
+std::string
+TransactionData::write_message_node_no_content(TSMBuffer &buffer, TSMLoc &hdr_loc)
+{
+  std::string result = "{";
+  int len            = 0;
+  char const *cp     = nullptr;
+  TSMLoc url_loc     = nullptr;
+
+  // Log scheme+method+request-target or status+reason based on header type
+  if (TSHttpHdrTypeGet(buffer, hdr_loc) == TS_HTTP_TYPE_REQUEST) {
+    // 1. "version"
+    int version = TSHttpHdrVersionGet(buffer, hdr_loc);
+    result += R"("version":")" + std::to_string(TS_HTTP_MAJOR(version)) + "." + std::to_string(TS_HTTP_MINOR(version)) + '"';
+
+    TSAssert(TS_SUCCESS == TSHttpHdrUrlGet(buffer, hdr_loc, &url_loc));
+    // 2. "scheme":
+    cp = TSUrlSchemeGet(buffer, url_loc, &len);
+    TSDebug(debug_tag, "write_message_node(): found scheme %.*s ", len, cp);
+    result += "," + traffic_dump::json_entry("scheme", cp, len);
+
+    // 3. "method":(string)
+    cp = TSHttpHdrMethodGet(buffer, hdr_loc, &len);
+    TSDebug(debug_tag, "write_message_node(): found method %.*s ", len, cp);
+    result += "," + traffic_dump::json_entry("method", cp, len);
+
+    // 4. "url"
+    cp = TSUrlHostGet(buffer, url_loc, &len);
+    std::string_view host{cp, static_cast<size_t>(len)};
+
+    char *url = TSUrlStringGet(buffer, url_loc, &len);
+    std::string_view url_string{url, static_cast<size_t>(len)};
+
+    if (host.empty()) {
+      // TSUrlStringGet will add the scheme to the URL, even if the request
+      // target doesn't contain it. However, we cannot just always remove the
+      // scheme because the original request target may include it. We assume
+      // here that a URL with a scheme but not a host is artificial and thus
+      // we remove it.
+      url_string = remove_scheme_prefix(url_string);
+    }
+
+    TSDebug(debug_tag, "write_message_node(): found host target %.*s", static_cast<int>(url_string.size()), url_string.data());
+    result += "," + traffic_dump::json_entry("url", url_string);
+    TSfree(url);
+    TSHandleMLocRelease(buffer, hdr_loc, url_loc);
+  } else {
+    // 1. "status":(string)
+    result += R"("status":)" + std::to_string(TSHttpHdrStatusGet(buffer, hdr_loc));
+    // 2. "reason":(string)
+    cp = TSHttpHdrReasonGet(buffer, hdr_loc, &len);
+    result += "," + traffic_dump::json_entry("reason", cp, len);
+    // 3. "encoding"
+  }
+
+  // "headers": [[name(string), value(string)]]
+  result += R"(,"headers":{"encoding":"esc_json", "fields": [)";
+  TSMLoc field_loc = TSMimeHdrFieldGet(buffer, hdr_loc, 0);
+  while (field_loc) {
+    TSMLoc next_field_loc;
+    char const *name  = nullptr;
+    char const *value = nullptr;
+    int name_len = 0, value_len = 0;
+    // Append to "fields" list if valid value exists
+    if ((name = TSMimeHdrFieldNameGet(buffer, hdr_loc, field_loc, &name_len)) && name_len) {
+      std::string_view name_view{name, static_cast<size_t>(name_len)};
+      value = TSMimeHdrFieldValueStringGet(buffer, hdr_loc, field_loc, -1, &value_len);
+      std::string_view value_view{value, static_cast<size_t>(value_len)};
+      std::string_view new_value = replace_sensitive_fields(name_view, value_view);
+      result += traffic_dump::json_entry_array(name_view, new_value);
+    }
+
+    next_field_loc = TSMimeHdrFieldNext(buffer, hdr_loc, field_loc);
+    TSHandleMLocRelease(buffer, hdr_loc, field_loc);
+    if ((field_loc = next_field_loc) != nullptr) {
+      result += ",";
+    }
+  }
+  return result += "]}";
+}
+
+std::string
+TransactionData::write_message_node(TSMBuffer &buffer, TSMLoc &hdr_loc, int64_t num_body_bytes)
+{
+  std::string result = write_message_node_no_content(buffer, hdr_loc);
+  result += write_content_node(num_body_bytes);
+  return result + "}";
+}
+
+std::string_view
+TransactionData::remove_scheme_prefix(std::string_view url)
+{
+  const auto scheme_separator = url.find("://");
+  if (scheme_separator == std::string::npos) {
+    return url;
+  }
+  url.remove_prefix(scheme_separator + 3);
+  return url;
+}
+
+bool
+TransactionData::init_helper()
+{
+  initialize_default_sensitive_field();
+  const std::string sensitive_fields_string = get_sensitive_field_description();
+  TSDebug(debug_tag, "Sensitive fields for which generic values will be dumped: %s", sensitive_fields_string.c_str());
+
+  if (TS_SUCCESS !=
+      TSUserArgIndexReserve(TS_USER_ARGS_TXN, traffic_dump::debug_tag, "Track transaction related data", &transaction_arg_index)) {
+    TSError("[%s] Unable to initialize plugin (disabled). Failed to reserve transaction arg.", traffic_dump::debug_tag);
+    return false;
+  }
+
+  // Register the collecting of client-request headers at the global level so
+  // we can process requests before other plugins. (Global hooks are
+  // processed before session and transaction ones.)
+  TSCont txn_cont = TSContCreate(global_transaction_handler, nullptr);
+  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, txn_cont);
+  return true;
+}
+
+// Transaction handler: writes headers to the log file using AIO
+int
+TransactionData::global_transaction_handler(TSCont contp, TSEvent event, void *edata)
+{
+  TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
+
+  // Retrieve SessionData
+  TSHttpSsn ssnp       = TSHttpTxnSsnGet(txnp);
+  SessionData *ssnData = static_cast<SessionData *>(TSUserArgGet(ssnp, SessionData::get_session_arg_index()));
+
+  // If no valid ssnData, continue transaction as if nothing happened. This transaction
+  // must be filtered out by our filter criteria.
+  if (!ssnData) {
+    TSDebug(debug_tag, "session_txn_handler(): No ssnData found. Abort.");
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+    return TS_SUCCESS;
+  }
+
+  switch (event) {
+  case TS_EVENT_HTTP_TXN_START: {
+    // We will piece together JSON content accumulated across several hooks of
+    // the transaction. The catch is that hooks across transactions in a
+    // session may fire interleaved in HTTP/2. Thus, in order to get
+    // non-garbled JSON content, we accumulate the data for an entire
+    // transaction and write that atomically once the transaction is completed.
+    TransactionData *txnData = new TransactionData;
+    TSUserArgSet(txnp, transaction_arg_index, txnData);
+    // Get UUID
+    char uuid[TS_CRUUID_STRING_LEN + 1];
+    TSAssert(TS_SUCCESS == TSClientRequestUuidGet(txnp, uuid));
+    std::string_view uuid_view{uuid, strnlen(uuid, TS_CRUUID_STRING_LEN)};
+
+    // Generate per transaction json records
+    txnData->txn_json += "{";
+    // "connection-time":(number)
+    TSHRTime start_time;
+    TSHttpTxnMilestoneGet(txnp, TS_MILESTONE_UA_BEGIN, &start_time);
+    txnData->txn_json += "\"connection-time\":" + std::to_string(start_time);
+
+    // "uuid":(string)
+    // The uuid is a header field for each message in the transaction. Use the
+    // "all" node to apply to each message.
+    std::string_view name = "uuid";
+    txnData->txn_json += ",\"all\":{\"headers\":{\"fields\":[" + json_entry_array(name, uuid_view);
+    txnData->txn_json += "]}}";
+    break;
+  }
+
+  case TS_EVENT_HTTP_READ_REQUEST_HDR: {
+    TransactionData *txnData = static_cast<TransactionData *>(TSUserArgGet(txnp, transaction_arg_index));
+    if (!txnData) {
+      TSError("[%s] No transaction data found for the header hook we registered for.", traffic_dump::debug_tag);
+      break;
+    }
+    // This hook is registered globally, not at TS_EVENT_HTTP_SSN_START in
+    // global_session_handler(). As such, this handler will be called with every
+    // transaction. However, we know that we are dumping this transaction
+    // because there is a ssnData associated with it.
+
+    // We must grab the client request information before remap happens because
+    // the remap process modifies the request buffer.
+    TSMBuffer buffer;
+    TSMLoc hdr_loc;
+    if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
+      TSDebug(debug_tag, "Found client request");
+      // We don't have an accurate view of the body size until TXN_CLOSE so we hold
+      // off on writing the content:size node until then.
+      txnData->txn_json += R"(,"client-request":)" + txnData->write_message_node_no_content(buffer, hdr_loc);
+      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+      buffer = nullptr;
+    }
+    break;
+  }
+
+  case TS_EVENT_HTTP_TXN_CLOSE: {
+    TransactionData *txnData = static_cast<TransactionData *>(TSUserArgGet(txnp, SessionData::get_session_arg_index()));
+    if (!txnData) {
+      TSError("[%s] No transaction data found for the close hook we registered for.", traffic_dump::debug_tag);
+      break;
+    }
+    // proxy-request/response headers
+    TSMBuffer buffer;
+    TSMLoc hdr_loc;
+    if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
+      txnData->txn_json += txnData->write_content_node(TSHttpTxnClientReqBodyBytesGet(txnp)) + "}";
+      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+      buffer = nullptr;
+    }
+    if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &buffer, &hdr_loc)) {
+      TSDebug(debug_tag, "Found proxy request");
+      txnData->txn_json +=
+        R"(,"proxy-request":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerReqBodyBytesGet(txnp));
+      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+      buffer = nullptr;
+    }
+    if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &buffer, &hdr_loc)) {
+      TSDebug(debug_tag, "Found server response");
+      txnData->txn_json +=
+        R"(,"server-response":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerRespBodyBytesGet(txnp));
+      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+      buffer = nullptr;
+    }
+    if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &buffer, &hdr_loc)) {
+      TSDebug(debug_tag, "Found proxy response");
+      txnData->txn_json +=
+        R"(,"proxy-response":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnClientRespBodyBytesGet(txnp));
+      TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+      buffer = nullptr;
+    }
+
+    txnData->txn_json += "}";
+    ssnData->write_transaction_to_disk(txnData->txn_json);
+    delete txnData;
+    break;
+  }
+  default:
+    TSDebug(debug_tag, "session_txn_handler(): Unhandled events %d", event);
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
+    return TS_ERROR;
+  }
+
+  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+  return TS_SUCCESS;
+}
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/transaction_data.h b/plugins/experimental/traffic_dump/transaction_data.h
new file mode 100644
index 0000000..c5b5139
--- /dev/null
+++ b/plugins/experimental/traffic_dump/transaction_data.h
@@ -0,0 +1,110 @@
+/** @txn_data.h
+  Traffic Dump data specific to transactions.
+  @section license License
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+      http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+#include "ts/ts.h"
+
+#include "sensitive_fields.h"
+
+namespace traffic_dump
+{
+/** The information associated with an single transaction.
+ *
+ * This class is responsible for containing the members associated with a
+ * particular transaction and defines the transaction handler callback.
+ */
+class TransactionData
+{
+private:
+  /** The string for the JSON content of this transaction. */
+  std::string txn_json;
+
+  // The index to be used for the TS API for storing this TransactionData on a
+  // per-transaction basis.
+  static int transaction_arg_index;
+
+  /// The set of fields, default and user-specified, that are sensitive and
+  /// whose values will be replaced with auto-generated generic content.
+  static sensitive_fields_t sensitive_fields;
+
+public:
+  /** Initialize TransactionData, using the provided sensitive fields.
+   *
+   * @return True if initialization is successful, false otherwise.
+   */
+  static bool init(sensitive_fields_t &&sensitive_fields);
+
+  /** Initialize TransactionData, using default sensitive fields.
+   * @return True if initialization is successful, false otherwise.
+   */
+  static bool init();
+
+  /// Read the txn information from TSMBuffer and write the header information.
+  /// This function does not write the content node.
+  std::string write_message_node_no_content(TSMBuffer &buffer, TSMLoc &hdr_loc);
+
+  /// Read the txn information from TSMBuffer and write the header information including
+  /// the content node describing the body characteristics.
+  std::string write_message_node(TSMBuffer &buffer, TSMLoc &hdr_loc, int64_t num_body_bytes);
+
+  /// The handler callback for transaction events.
+  static int global_transaction_handler(TSCont contp, TSEvent event, void *edata);
+
+private:
+  /** Common logic for the init overloads. */
+  static bool init_helper();
+
+  /** Initialize the generic sensitive field to be dumped. This is used instead
+   * of the sensitive field values seen on the wire.
+   */
+  static void initialize_default_sensitive_field();
+
+  /** Return a separated string representing the HTTP fields considered sensitive.
+   *
+   * @return A comma-separated string representing the sensitive HTTP fields.
+   */
+  static std::string get_sensitive_field_description();
+
+  /** Inspect the field to see whether it is sensitive and return a generic value
+   * of equal size to the original if it is.
+   *
+   * @param[in] name The field name to inspect.
+   * @param[in] original_value The field value to inspect.
+   *
+   * @return The value traffic_dump should dump for the given field.
+   */
+  std::string_view replace_sensitive_fields(std::string_view name, std::string_view original_value);
+
+  /// Write the content JSON node for an HTTP message.
+  //
+  /// "content"
+  ///    "encoding"
+  ///    "size"
+  std::string write_content_node(int64_t num_body_bytes);
+
+  /** Remove the scheme prefix from the url.
+   *
+   * @return The view without the scheme prefix.
+   */
+  std::string_view remove_scheme_prefix(std::string_view url);
+};
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/unit_tests/test_json_utils.cc b/plugins/experimental/traffic_dump/unit_tests/test_json_utils.cc
new file mode 100644
index 0000000..4c27a91
--- /dev/null
+++ b/plugins/experimental/traffic_dump/unit_tests/test_json_utils.cc
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "json_utils.h"
+
+#include <cstring>
+#include <catch.hpp>
+
+using namespace traffic_dump;
+
+TEST_CASE("JsonUtilsMap", "[json_entry]")
+{
+  SECTION("Test the string_view value overload")
+  {
+    CHECK(std::string(R"("name":"value")") == json_entry("name", "value"));
+    CHECK(std::string(R"("":"value")") == json_entry("", "value"));
+    CHECK(std::string(R"("name":"")") == json_entry("name", ""));
+  }
+
+  SECTION("Test the char buffer value overload")
+  {
+    CHECK(std::string(R"("name":"value")") == json_entry("name", "value", strlen("value")));
+    CHECK(std::string(R"("name":"val")") == json_entry("name", "value", 3));
+    CHECK(std::string(R"("":"value")") == json_entry("", "value", strlen("value")));
+    CHECK(std::string(R"("name":"")") == json_entry("name", "", 0));
+  }
+
+  SECTION("Test that escaped characters are encoded as expected")
+  {
+    // Note that the raw strings on the left, i.e., R"(...)", leaves "\b" as
+    // two characters, not a single escaped one. The escaped characters on
+    // the right, by contrast, such as '\b', is a single escaped character.
+    CHECK(std::string(R"("name":"val\bue")") == json_entry("name", "val\bue"));
+    CHECK(std::string(R"("name":"\\value")") == json_entry("name", "\\value"));
+    CHECK(std::string(R"("name":"value\f")") == json_entry("name", "value\f"));
+    CHECK(std::string(R"("na\rme":"\tva\nlue\f")") == json_entry("na\rme", "\tva\nlue\f"));
+    CHECK(std::string(R"("\r":"\t\n\f")") == json_entry("\r", "\t\n\f"));
+  }
+}
+
+TEST_CASE("JsonUtilsArray", "[json_entry_array]")
+{
+  CHECK(std::string(R"(["name","value"])") == json_entry_array("name", "value"));
+}
diff --git a/plugins/experimental/traffic_dump/unit_tests/test_sensitive_fields.cc b/plugins/experimental/traffic_dump/unit_tests/test_sensitive_fields.cc
new file mode 100644
index 0000000..19845c9
--- /dev/null
+++ b/plugins/experimental/traffic_dump/unit_tests/test_sensitive_fields.cc
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "sensitive_fields.h"
+
+#include <cstring>
+#include <catch.hpp>
+
+using namespace traffic_dump;
+
+TEST_CASE("SensitiveFields", "[sensitive_fields_t]")
+{
+  sensitive_fields_t test_fields = {"one", "tWO", "THREE"};
+  CHECK(test_fields.count("one") == 1);
+  CHECK(test_fields.count("oNe") == 1);
+
+  CHECK(test_fields.count("tWO") == 1);
+  CHECK(test_fields.count("two") == 1);
+
+  CHECK(test_fields.count("THREE") == 1);
+  CHECK(test_fields.count("three") == 1);
+}
diff --git a/plugins/experimental/traffic_dump/unit_tests/unit_test_main.cc b/plugins/experimental/traffic_dump/unit_tests/unit_test_main.cc
new file mode 100644
index 0000000..6aed3a6
--- /dev/null
+++ b/plugins/experimental/traffic_dump/unit_tests/unit_test_main.cc
@@ -0,0 +1,25 @@
+/** @file
+
+  This file used for catch based tests. It is the main() stub.
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#define CATCH_CONFIG_MAIN
+#include "catch.hpp"
diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold
index 0ac91ba..1fe8660 100644
--- a/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold
+++ b/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold
@@ -8,6 +8,6 @@
 < Content-Length: 0
 < Set-Cookie: classified_not_for_logging
 < Date: ``
-< Age: ``
+``
 < Server: ATS/``
 ``
diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold
new file mode 100644
index 0000000..81846e6
--- /dev/null
+++ b/tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold
@@ -0,0 +1,7 @@
+``
+> GET /cache_test HTTP/1.1
+``
+< HTTP/1.1 200 OK
+< Cache-Control: max-age=300
+< Content-Length: 4
+``
diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold
new file mode 100644
index 0000000..9ac048c
--- /dev/null
+++ b/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold
@@ -0,0 +1,11 @@
+``
+> GET /first HTTP/1.1
+> Host: www.example.com
+``
+< HTTP/1.1 200 OK
+``
+> GET /second HTTP/1.1
+> Host: www.example.com
+``
+< HTTP/1.1 200 OK
+``
diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py
index 5cf9f24..7ba42f2 100644
--- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py
+++ b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py
@@ -29,7 +29,7 @@ Test.SkipUnless(
 # Configure the origin server.
 server = Test.MakeOriginServer("server")
 
-request_header = {"headers": "GET / HTTP/1.1\r\n"
+request_header = {"headers": "GET /one HTTP/1.1\r\n"
                   "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
                   "timestamp": "1469733493.993", "body": ""}
 response_header = {"headers": "HTTP/1.1 200 OK"
@@ -37,7 +37,15 @@ response_header = {"headers": "HTTP/1.1 200 OK"
                    "\r\nSet-Cookie: classified_not_for_logging\r\n\r\n",
                    "timestamp": "1469733493.993", "body": ""}
 server.addResponse("sessionfile.log", request_header, response_header)
-request_header = {"headers": "GET /one HTTP/1.1\r\n"
+request_header = {"headers": "GET /two HTTP/1.1\r\n"
+                  "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+                  "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+                   "\r\nConnection: close\r\nContent-Length: 0"
+                   "\r\nSet-Cookie: classified_not_for_logging\r\n\r\n",
+                   "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /three HTTP/1.1\r\n"
                   "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
                   "timestamp": "1469733493.993", "body": ""}
 response_header = {"headers": "HTTP/1.1 200 OK"
@@ -52,13 +60,38 @@ response_header = {"headers": "HTTP/1.1 200 OK"
                    "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
                    "timestamp": "1469733493.993", "body": ""}
 server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /cache_test HTTP/1.1\r\n"
+                  "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+                  "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+                   "\r\nConnection: close\r\nCache-Control: max-age=300\r\n"
+                   "Content-Length: 4\r\n\r\n",
+                   "timestamp": "1469733493.993", "body": "1234"}
+server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /first HTTP/1.1\r\n"
+                  "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+                  "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+                   "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
+                   "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /second HTTP/1.1\r\n"
+                  "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+                  "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+                   "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
+                   "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionfile.log", request_header, response_header)
 
-# Define ATS and configure
+# Define ATS and configure it.
 ts = Test.MakeATSProcess("ts")
 replay_dir = os.path.join(ts.RunDirectory, "ts", "log")
 ts.Disk.records_config.update({
     'proxy.config.diags.debug.enabled': 1,
     'proxy.config.diags.debug.tags': 'traffic_dump',
+    'proxy.config.http.cache.http': 1,
+    'proxy.config.http.wait_for_cache': 1,
+    'proxy.config.http.insert_age_in_response': 0,
 })
 ts.Disk.remap_config.AddLine(
     'map / http://127.0.0.1:{0}'.format(server.Variables.Port)
@@ -68,6 +101,17 @@ ts.Disk.plugin_config.AddLine(
     'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 '
     '--sensitive-fields "cookie,set-cookie,x-request-1,x-request-2"'.format(replay_dir)
 )
+# Configure logging of transactions. This is helpful for the cache test below.
+ts.Disk.logging_yaml.AddLines(
+    '''
+logging:
+  formats:
+    - name: basic
+      format: "%<cluc>: Read result: %<crc>:%<crsc>:%<chm>, Write result: %<cwr>"
+  logs:
+    - filename: transactions
+      format: basic
+'''.split('\n'))
 
 # Set up trafficserver expectations.
 ts.Disk.diags_log.Content = Testers.ContainsExpression(
@@ -92,6 +136,12 @@ replay_file_session_3 = os.path.join(replay_dir, "127", "0000000000000002")
 ts.Disk.File(replay_file_session_3, exists=True)
 replay_file_session_4 = os.path.join(replay_dir, "127", "0000000000000003")
 ts.Disk.File(replay_file_session_4, exists=True)
+replay_file_session_5 = os.path.join(replay_dir, "127", "0000000000000004")
+ts.Disk.File(replay_file_session_5, exists=True)
+replay_file_session_6 = os.path.join(replay_dir, "127", "0000000000000005")
+ts.Disk.File(replay_file_session_6, exists=True)
+replay_file_session_7 = os.path.join(replay_dir, "127", "0000000000000006")
+ts.Disk.File(replay_file_session_7, exists=True)
 
 #
 # Test 1: Verify the correct behavior of two transactions across two sessions.
@@ -103,19 +153,19 @@ tr = Test.AddTestRun("First transaction")
 tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port))
 tr.Processes.Default.StartBefore(Test.Processes.ts)
 tr.Processes.Default.Command = \
-        ('curl --http1.1 http://127.0.0.1:{0} -H"Cookie: donotlogthis" '
+        ('curl --http1.1 http://127.0.0.1:{0}/one -H"Cookie: donotlogthis" '
          '-H"Host: www.example.com" -H"X-Request-1: ultra_sensitive" --verbose'.format(
              ts.Variables.port))
 tr.Processes.Default.ReturnCode = 0
 tr.Processes.Default.Streams.stderr = "gold/200.gold"
 tr.StillRunningAfter = server
 tr.StillRunningAfter = ts
-session_1_protocols = "tcp,ipv4"
+http_protocols = "tcp,ipv4"
 
 # Execute the second transaction.
 tr = Test.AddTestRun("Second transaction")
 tr.Processes.Default.Command = \
-        ('curl http://127.0.0.1:{0}/one -H"Host: www.example.com" '
+        ('curl http://127.0.0.1:{0}/two -H"Host: www.example.com" '
          '-H"X-Request-2: also_very_sensitive" --verbose'.format(
             ts.Variables.port))
 tr.Processes.Default.ReturnCode = 0
@@ -137,7 +187,7 @@ tr.Processes.Default.Command = 'python3 {0} {1} {2} {3} --client-protocols "{4}"
         os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
         replay_file_session_1,
         sensitive_fields_arg,
-        session_1_protocols)
+        http_protocols)
 tr.Processes.Default.ReturnCode = 0
 tr.StillRunningAfter = server
 tr.StillRunningAfter = ts
@@ -145,7 +195,7 @@ tr.StillRunningAfter = ts
 # Verify the properties of the replay file for the second transaction.
 tr = Test.AddTestRun("Verify the json content of the second session")
 tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
-tr.Processes.Default.Command = "python3 {0} {1} {2} {3} --request-target '/one'".format(
+tr.Processes.Default.Command = "python3 {0} {1} {2} {3} --request-target '/two'".format(
         verify_replay,
         os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
         replay_file_session_2,
@@ -163,7 +213,7 @@ tr = Test.AddTestRun("Make a request with an explicit target.")
 request_target = "http://localhost:{0}/candy".format(ts.Variables.port)
 tr.Processes.Default.Command = (
         'curl --request-target "{0}" '
-        'http://127.0.0.1:{1} -H"Host: www.example.com" --verbose'.format(
+        'http://127.0.0.1:{1}/three -H"Host: www.example.com" --verbose'.format(
             request_target, ts.Variables.port))
 tr.Processes.Default.ReturnCode = 0
 tr.Processes.Default.Streams.stderr = "gold/explicit_target.gold"
@@ -187,7 +237,6 @@ tr.StillRunningAfter = ts
 # Test 3: Verify correct handling of a POST with body data.
 #
 
-# Verify that an explicit path in the request line is recorded.
 tr = Test.AddTestRun("Make a POST request with a body.")
 request_target = "http://localhost:{0}/post_with_body".format(ts.Variables.port)
 
@@ -216,3 +265,59 @@ tr.Processes.Default.Command = \
 tr.Processes.Default.ReturnCode = 0
 tr.StillRunningAfter = server
 tr.StillRunningAfter = ts
+
+#
+# Test 4: Verify correct handling of a response produced out of the cache.
+#
+tr = Test.AddTestRun("Make a request for an uncached object.")
+tr.Processes.Default.Command = \
+        ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.example.com" --verbose'.format(
+             ts.Variables.port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stderr = "gold/4_byte_response_body.gold"
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+tr = Test.AddTestRun("Repeat the previous request: should be cached now.")
+tr.Processes.Default.Command = \
+        ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.example.com" --verbose'.format(
+             ts.Variables.port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stderr = "gold/4_byte_response_body.gold"
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+tr = Test.AddTestRun("Verify that the cached response's replay file looks appropriate.")
+tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
+tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}"'.format(
+        verify_replay,
+        os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
+        replay_file_session_6,
+        http_protocols)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+#
+# Test 5: Verify correct handling of two transactions in a session.
+#
+tr = Test.AddTestRun("Conduct two transactions in the same session.")
+tr.Processes.Default.Command = \
+        ('curl --http1.1 http://127.0.0.1:{0}/first -H"Host: www.example.com" --verbose --next '
+            'curl --http1.1 http://127.0.0.1:{0}/second -H"Host: www.example.com" --verbose'
+            .format(ts.Variables.port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stderr = "gold/two_transactions.gold"
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+tr = Test.AddTestRun("Verify that the dump file can be read.")
+tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
+tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}"'.format(
+        verify_replay,
+        os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
+        replay_file_session_7,
+        http_protocols)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
diff --git a/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py b/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py
index b48ae4a..e48819c 100644
--- a/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py
+++ b/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py
@@ -51,8 +51,8 @@ def validate_json(schema_json, replay_json):
     """
     try:
         jsonschema.validate(instance=replay_json, schema=schema_json)
-    except jsonschema.ValidationError:
-        print("The replay file does not validate against the schema.")
+    except jsonschema.ValidationError as e:
+        print("The replay file does not validate against the schema: {}".format(e))
         return False
     else:
         return True
@@ -72,7 +72,7 @@ def verify_there_was_a_transaction(replay_json):
         print("There are no transactions in the replay file.")
         return False
     transaction = transactions[0]
-    if not ('client-request' in transaction and 'server-response' in transaction):
+    if not ('client-request' in transaction and 'proxy-response' in transaction):
         print("There was not request and response in the transaction of the replay file.")
         return False
 
diff --git a/tests/tools/lib/replay_schema.json b/tests/tools/lib/replay_schema.json
index 5e15281..328c13d 100644
--- a/tests/tools/lib/replay_schema.json
+++ b/tests/tools/lib/replay_schema.json
@@ -41,7 +41,7 @@
             "items": {
               "description": "Transaction",
               "type": "object",
-              "required": [ "client-request", "server-response" ],
+              "required": [ "client-request", "proxy-response" ],
               "properties": {
                 "uuid": {
                   "description": "UUID to identify this specific transaction.",