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

cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/10.2.x by this push:
     new ede68af1a9 Log malformed HTTP/2 requests (#13059) (#13105)
ede68af1a9 is described below

commit ede68af1a9cdbcfc434a6d3a00264f504cc5648b
Author: Brian Neradt <[email protected]>
AuthorDate: Tue Apr 21 15:56:37 2026 -0500

    Log malformed HTTP/2 requests (#13059) (#13105)
    
    Unlike HTTP/1 transactions, malformed HTTP/2 requests are rejected
    before HttpSM creation, so they bypassed the normal transaction logging
    path. That left malformed h2 traffic out of squid.log even when similar
    h1 failures were visible.
    
    This adds a pre-transaction LogAccess path for malformed h2 request
    headers and emits a best-effort access log entry before resetting the
    stream.
    
    (cherry picked from commit 05554caba9eff692313aa0d0e6eb7fbe1b0f3cea)
---
 include/proxy/PreTransactionLogData.h              | 218 ++++++
 include/proxy/ProxyTransaction.h                   |  13 +
 include/proxy/http/CompletedTransactionLogData.h   | 208 ++++++
 include/proxy/http2/Http2Stream.h                  |   6 +
 include/proxy/http3/Http3HeaderVIOAdaptor.h        |  13 +-
 include/proxy/logging/Log.h                        |   3 +-
 include/proxy/logging/LogAccess.h                  |  20 +-
 include/proxy/logging/TransactionLogData.h         | 607 +++++++++++++++
 src/proxy/ProxyTransaction.cc                      | 124 +++
 src/proxy/http/CMakeLists.txt                      |   1 +
 src/proxy/http/CompletedTransactionLogData.cc      | 832 +++++++++++++++++++++
 src/proxy/http/HttpBodyFactory.cc                  |   4 +-
 src/proxy/http/HttpSM.cc                           |   7 +-
 src/proxy/http2/Http2ConnectionState.cc            |   6 +
 src/proxy/http3/Http3HeaderVIOAdaptor.cc           |  11 +-
 src/proxy/http3/Http3Transaction.cc                |   2 +-
 src/proxy/logging/CMakeLists.txt                   |   4 +
 src/proxy/logging/LogAccess.cc                     | 744 ++++++++----------
 src/proxy/logging/unit-tests/test_LogAccess.cc     | 211 ++++++
 .../connect/h2_malformed_request_logging.test.py   | 195 +++++
 .../connect/malformed_h2_request_client.py         | 172 +++++
 .../h2_malformed_request_logging.replay.yaml       |  90 +++
 22 files changed, 3039 insertions(+), 452 deletions(-)

diff --git a/include/proxy/PreTransactionLogData.h 
b/include/proxy/PreTransactionLogData.h
new file mode 100644
index 0000000000..9d65e66761
--- /dev/null
+++ b/include/proxy/PreTransactionLogData.h
@@ -0,0 +1,218 @@
+/** @file
+
+  PreTransactionLogData populates LogData for requests that fail before
+  HttpSM creation.
+
+  @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 "proxy/logging/TransactionLogData.h"
+
+#include <string>
+
+/** Populate LogData for requests that never created an HttpSM.
+ *
+ * Malformed HTTP/2 or HTTP/3 request headers can be rejected while the
+ * connection is still decoding and validating the stream, before the request
+ * progresses far enough to create an HttpSM.  This class carries the
+ * copied request and session metadata needed to emit a best-effort
+ * transaction log entry for those failures.
+ *
+ * Unlike TransactionLogData (which reads from a live HttpSM), this class
+ * owns its milestones, addresses, and strings because the originating
+ * stream is about to be destroyed.
+ */
+class PreTransactionLogData : public TransactionLogData
+{
+public:
+  PreTransactionLogData() = default;
+
+  ~PreTransactionLogData() override
+  {
+    if (owned_client_request.valid()) {
+      owned_client_request.destroy();
+    }
+  }
+
+  // ===== Milestones =====
+
+  TransactionMilestones const *
+  get_milestones() const override
+  {
+    return &owned_milestones;
+  }
+
+  // ===== Headers =====
+
+  HTTPHdr *
+  get_client_request() const override
+  {
+    if (owned_client_request.valid()) {
+      return const_cast<HTTPHdr *>(&owned_client_request);
+    }
+    return nullptr;
+  }
+
+  // ===== Client request URL / path =====
+
+  const char *
+  get_client_req_url_str() const override
+  {
+    return owned_url.empty() ? nullptr : owned_url.c_str();
+  }
+  int
+  get_client_req_url_len() const override
+  {
+    return static_cast<int>(owned_url.size());
+  }
+  const char *
+  get_client_req_url_path_str() const override
+  {
+    return owned_path.empty() ? nullptr : owned_path.c_str();
+  }
+  int
+  get_client_req_url_path_len() const override
+  {
+    return static_cast<int>(owned_path.size());
+  }
+
+  // ===== Client addressing =====
+
+  sockaddr const *
+  get_client_addr() const override
+  {
+    return &owned_client_addr.sa;
+  }
+  sockaddr const *
+  get_client_src_addr() const override
+  {
+    return &owned_client_src_addr.sa;
+  }
+  sockaddr const *
+  get_client_dst_addr() const override
+  {
+    return &owned_client_dst_addr.sa;
+  }
+  uint16_t
+  get_client_port() const override
+  {
+    return m_client_port;
+  }
+
+  // ===== Squid codes =====
+
+  SquidLogCode
+  get_log_code() const override
+  {
+    return m_log_code;
+  }
+  SquidHitMissCode
+  get_hit_miss_code() const override
+  {
+    return m_hit_miss_code;
+  }
+  SquidHierarchyCode
+  get_hier_code() const override
+  {
+    return m_hier_code;
+  }
+
+  // ===== Transaction identifiers =====
+
+  int64_t
+  get_connection_id() const override
+  {
+    return m_connection_id;
+  }
+  int
+  get_transaction_id() const override
+  {
+    return m_transaction_id;
+  }
+
+  // ===== Protocol info =====
+
+  const char *
+  get_client_protocol() const override
+  {
+    return owned_client_protocol_str.empty() ? nullptr : 
owned_client_protocol_str.c_str();
+  }
+
+  // ===== Connection flags =====
+
+  bool
+  get_client_connection_is_ssl() const override
+  {
+    return m_client_connection_is_ssl;
+  }
+
+  // ===== Server transaction count =====
+
+  int64_t
+  get_server_transact_count() const override
+  {
+    return m_server_transact_count;
+  }
+
+  // ===== Fallback fields for pre-transaction logging =====
+
+  std::string_view
+  get_method() const override
+  {
+    return owned_method;
+  }
+  std::string_view
+  get_scheme() const override
+  {
+    return owned_scheme;
+  }
+  std::string_view
+  get_client_protocol_str() const override
+  {
+    return owned_client_protocol_str;
+  }
+
+  // ===== Owned backing storage (public for ProxyTransaction to populate). 
=====
+
+  HTTPHdr               owned_client_request;
+  TransactionMilestones owned_milestones;
+  IpEndpoint            owned_client_addr     = {};
+  IpEndpoint            owned_client_src_addr = {};
+  IpEndpoint            owned_client_dst_addr = {};
+
+  std::string owned_method;
+  std::string owned_scheme;
+  std::string owned_authority;
+  std::string owned_path;
+  std::string owned_url;
+  std::string owned_client_protocol_str;
+
+  // ===== Simple fields (public for ProxyTransaction to set). =====
+
+  uint16_t           m_client_port              = 0;
+  SquidLogCode       m_log_code                 = SquidLogCode::EMPTY;
+  SquidHitMissCode   m_hit_miss_code            = SQUID_MISS_NONE;
+  SquidHierarchyCode m_hier_code                = SquidHierarchyCode::NONE;
+  int64_t            m_connection_id            = 0;
+  int                m_transaction_id           = 0;
+  bool               m_client_connection_is_ssl = false;
+  int64_t            m_server_transact_count    = 0;
+};
diff --git a/include/proxy/ProxyTransaction.h b/include/proxy/ProxyTransaction.h
index c81f73be98..7316be293a 100644
--- a/include/proxy/ProxyTransaction.h
+++ b/include/proxy/ProxyTransaction.h
@@ -146,6 +146,19 @@ public:
 
   void mark_as_tunnel_endpoint() override;
 
+  /** Emit a best-effort access log entry for a request that failed before
+   *  HttpSM creation.
+   *
+   * Call this when a malformed request is rejected at the protocol layer
+   * (e.g. during HTTP/2 or HTTP/3 header decoding) and no HttpSM was
+   * created.  The method populates a PreTransactionLogData from the
+   * session and the partially decoded request, then invokes Log::access.
+   *
+   * @param[in] request The decoded (possibly partial) request header.
+   * @param[in] protocol_str Protocol string for the log entry (e.g. "http/2").
+   */
+  void log_pre_transaction_access(HTTPHdr const *request, const char 
*protocol_str);
+
   /// Variables
   //
   HttpSessionAccept::Options upstream_outbound_options; // overwritable copy 
of options
diff --git a/include/proxy/http/CompletedTransactionLogData.h 
b/include/proxy/http/CompletedTransactionLogData.h
new file mode 100644
index 0000000000..3e1a25aa7b
--- /dev/null
+++ b/include/proxy/http/CompletedTransactionLogData.h
@@ -0,0 +1,208 @@
+/** @file
+
+  CompletedTransactionLogData populates TransactionLogData from a live HttpSM.
+
+  @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 "proxy/logging/TransactionLogData.h"
+
+class HttpSM;
+
+/** Provide TransactionLogData from a live HttpSM via virtual getters.
+ *
+ * Each getter reads directly from m_http_sm on demand, avoiding the need to
+ * copy all fields upfront. The HttpSM must outlive this object.
+ */
+class CompletedTransactionLogData : public TransactionLogData
+{
+public:
+  /** Construct from a live HttpSM.
+   *
+   * @param[in] sm The HttpSM for the completing transaction.
+   */
+  explicit CompletedTransactionLogData(HttpSM *sm);
+
+  void *http_sm_for_plugins() const override;
+
+  // ===== Milestones =====
+  TransactionMilestones const *get_milestones() const override;
+
+  // ===== Headers =====
+  HTTPHdr *get_client_request() const override;
+  HTTPHdr *get_proxy_response() const override;
+  HTTPHdr *get_proxy_request() const override;
+  HTTPHdr *get_server_response() const override;
+  HTTPHdr *get_cache_response() const override;
+
+  // ===== Client request URL / path =====
+  const char *get_client_req_url_str() const override;
+  int         get_client_req_url_len() const override;
+  const char *get_client_req_url_path_str() const override;
+  int         get_client_req_url_path_len() const override;
+
+  // ===== Proxy response content-type / reason =====
+  char *get_proxy_resp_content_type_str() const override;
+  int   get_proxy_resp_content_type_len() const override;
+  char *get_proxy_resp_reason_phrase_str() const override;
+  int   get_proxy_resp_reason_phrase_len() const override;
+
+  // ===== Unmapped URL =====
+  char *get_unmapped_url_str() const override;
+  int   get_unmapped_url_len() const override;
+
+  // ===== Cache lookup URL =====
+  char *get_cache_lookup_url_str() const override;
+  int   get_cache_lookup_url_len() const override;
+
+  // ===== Client addressing =====
+  sockaddr const *get_client_addr() const override;
+  sockaddr const *get_client_src_addr() const override;
+  sockaddr const *get_client_dst_addr() const override;
+  sockaddr const *get_verified_client_addr() const override;
+  uint16_t        get_client_port() const override;
+
+  // ===== Server addressing =====
+  sockaddr const *get_server_src_addr() const override;
+  sockaddr const *get_server_dst_addr() const override;
+  sockaddr const *get_server_info_dst_addr() const override;
+  const char     *get_server_name() const override;
+
+  // ===== Squid codes =====
+  SquidLogCode       get_log_code() const override;
+  SquidSubcode       get_subcode() const override;
+  SquidHitMissCode   get_hit_miss_code() const override;
+  SquidHierarchyCode get_hier_code() const override;
+
+  // ===== Byte counters =====
+  int64_t get_client_request_body_bytes() const override;
+  int64_t get_client_response_hdr_bytes() const override;
+  int64_t get_client_response_body_bytes() const override;
+  int64_t get_server_request_body_bytes() const override;
+  int64_t get_server_response_body_bytes() const override;
+  int64_t get_cache_response_body_bytes() const override;
+  int64_t get_cache_response_hdr_bytes() const override;
+
+  // ===== Transaction identifiers =====
+  int64_t get_sm_id() const override;
+  int64_t get_connection_id() const override;
+  int     get_transaction_id() const override;
+  int     get_transaction_priority_weight() const override;
+  int     get_transaction_priority_dependence() const override;
+
+  // ===== Plugin info =====
+  int64_t     get_plugin_id() const override;
+  const char *get_plugin_tag() const override;
+
+  // ===== Protocol info =====
+  const char *get_client_protocol() const override;
+  const char *get_server_protocol() const override;
+  const char *get_client_sec_protocol() const override;
+  const char *get_client_cipher_suite() const override;
+  const char *get_client_curve() const override;
+  const char *get_client_security_group() const override;
+  int         get_client_alpn_id() const override;
+
+  // ===== SNI =====
+  const char *get_sni_server_name() const override;
+
+  // ===== Connection flags =====
+  bool get_client_tcp_reused() const override;
+  bool get_client_connection_is_ssl() const override;
+  bool get_client_ssl_reused() const override;
+  int  get_client_ssl_resumption_type() const override;
+  bool get_is_internal() const override;
+  bool get_server_connection_is_ssl() const override;
+  bool get_server_ssl_reused() const override;
+  int  get_server_connection_provided_cert() const override;
+  int  get_client_provided_cert() const override;
+
+  // ===== Server transaction count =====
+  int64_t get_server_transact_count() const override;
+
+  // ===== Finish status =====
+  int get_client_finish_status_code() const override;
+  int get_proxy_finish_status_code() const override;
+
+  // ===== Error codes =====
+  const char *get_client_rx_error_code() const override;
+  const char *get_client_tx_error_code() const override;
+
+  // ===== MPTCP =====
+  std::optional<bool> get_mptcp_state() const override;
+
+  // ===== Misc transaction state =====
+  in_port_t get_incoming_port() const override;
+  int       get_orig_scheme() const override;
+  int64_t   get_congestion_control_crat() const override;
+
+  // ===== Cache state =====
+  int get_cache_write_code() const override;
+  int get_cache_transform_write_code() const override;
+  int get_cache_open_read_tries() const override;
+  int get_cache_open_write_tries() const override;
+  int get_max_cache_open_write_retries() const override;
+
+  // ===== Retry attempts =====
+  int64_t get_simple_retry_attempts() const override;
+  int64_t get_unavailable_retry_attempts() const override;
+  int64_t get_retry_attempts_saved() const override;
+
+  // ===== Status plugin entry name =====
+  std::string_view get_http_return_code_setter_name() const override;
+
+  // ===== Proxy Protocol =====
+  int              get_pp_version() const override;
+  sockaddr const  *get_pp_src_addr() const override;
+  sockaddr const  *get_pp_dst_addr() const override;
+  std::string_view get_pp_authority() const override;
+  std::string_view get_pp_tls_cipher() const override;
+  std::string_view get_pp_tls_version() const override;
+
+  // ===== Server response Transfer-Encoding =====
+  std::string_view get_server_response_transfer_encoding() const override;
+
+private:
+  HttpSM *m_http_sm;
+
+  // Cached values for fields that require computation or string formatting.
+  mutable char m_client_rx_error_code[10] = {'-', '\0'};
+  mutable char m_client_tx_error_code[10] = {'-', '\0'};
+  mutable bool m_error_codes_formatted    = false;
+
+  // Cached URL string pointers (computed on first access).
+  mutable const char *m_client_req_url_str      = nullptr;
+  mutable int         m_client_req_url_len      = 0;
+  mutable const char *m_client_req_url_path_str = nullptr;
+  mutable int         m_client_req_url_path_len = 0;
+  mutable bool        m_url_cached              = false;
+
+  // Cached content-type pointers (computed on first access).
+  mutable char *m_proxy_resp_content_type_str  = nullptr;
+  mutable int   m_proxy_resp_content_type_len  = 0;
+  mutable char *m_proxy_resp_reason_phrase_str = nullptr;
+  mutable int   m_proxy_resp_reason_phrase_len = 0;
+  mutable bool  m_content_type_cached          = false;
+
+  void cache_url_strings() const;
+  void cache_content_type() const;
+  void format_error_codes() const;
+};
diff --git a/include/proxy/http2/Http2Stream.h 
b/include/proxy/http2/Http2Stream.h
index 5d9beac647..bc4b0743c5 100644
--- a/include/proxy/http2/Http2Stream.h
+++ b/include/proxy/http2/Http2Stream.h
@@ -138,6 +138,12 @@ public:
     return &_send_header;
   }
 
+  HTTPHdr const *
+  get_receive_header() const
+  {
+    return &_receive_header;
+  }
+
   void update_read_length(int count);
   void set_read_done();
 
diff --git a/include/proxy/http3/Http3HeaderVIOAdaptor.h 
b/include/proxy/http3/Http3HeaderVIOAdaptor.h
index ca8b3da0e6..59a02b9a15 100644
--- a/include/proxy/http3/Http3HeaderVIOAdaptor.h
+++ b/include/proxy/http3/Http3HeaderVIOAdaptor.h
@@ -27,10 +27,12 @@
 #include "proxy/hdrs/VersionConverter.h"
 #include "proxy/http3/Http3FrameHandler.h"
 
+class HQTransaction;
+
 class Http3HeaderVIOAdaptor : public Continuation, public Http3FrameHandler
 {
 public:
-  Http3HeaderVIOAdaptor(VIO *sink, HTTPType http_type, QPACK *qpack, uint64_t 
stream_id);
+  Http3HeaderVIOAdaptor(VIO *sink, HTTPType http_type, QPACK *qpack, uint64_t 
stream_id, HQTransaction *txn = nullptr);
   ~Http3HeaderVIOAdaptor();
 
   // Http3FrameHandler
@@ -41,10 +43,11 @@ public:
   int  event_handler(int event, Event *data);
 
 private:
-  VIO     *_sink_vio    = nullptr;
-  QPACK   *_qpack       = nullptr;
-  uint64_t _stream_id   = 0;
-  bool     _is_complete = false;
+  VIO           *_sink_vio    = nullptr;
+  QPACK         *_qpack       = nullptr;
+  uint64_t       _stream_id   = 0;
+  bool           _is_complete = false;
+  HQTransaction *_txn         = nullptr;
 
   HTTPHdr          _header; ///< HTTP header buffer for decoding
   VersionConverter _hvc;
diff --git a/include/proxy/logging/Log.h b/include/proxy/logging/Log.h
index 7c76794687..09ce7ab2f7 100644
--- a/include/proxy/logging/Log.h
+++ b/include/proxy/logging/Log.h
@@ -43,7 +43,8 @@
   @section example Example usage of the API
 
   @code
-  LogAccess entry(this);
+  // Populate a LogData (e.g. TransactionLogData or PreTransactionLogData), 
then:
+  LogAccess entry(data);
   int ret = Log::access(&entry);
   @endcode
 
diff --git a/include/proxy/logging/LogAccess.h 
b/include/proxy/logging/LogAccess.h
index c062e5af0d..7fdc82851c 100644
--- a/include/proxy/logging/LogAccess.h
+++ b/include/proxy/logging/LogAccess.h
@@ -25,13 +25,16 @@
 #pragma once
 
 #include "tscore/ink_align.h"
+#include "proxy/Milestones.h"
+#include "proxy/hdrs/HTTP.h"
 #include "proxy/logging/LogField.h"
 
-class HTTPHdr;
-class HttpSM;
+class TransactionLogData;
 class IpClass;
 union IpEndpoint;
 
+#include <string>
+
 /*-------------------------------------------------------------------------
   LogAccess
 
@@ -114,7 +117,16 @@ class LogAccess
 {
 public:
   LogAccess() = delete;
-  explicit LogAccess(HttpSM *sm);
+
+  /** Construct from a TransactionLogData instance.
+   *
+   * The caller retains ownership of @a data, which must outlive the
+   * synchronous Log::access() call that marshals this entry.
+   *
+   * @param[in] data Populated TransactionLogData (CompletedTransactionLogData
+   *                 or PreTransactionLogData).
+   */
+  explicit LogAccess(TransactionLogData &data);
 
   ~LogAccess() {}
   void init();
@@ -371,7 +383,7 @@ public:
   LogAccess &operator=(LogAccess &rhs) = delete; // or assignment
 
 private:
-  HttpSM *m_http_sm = nullptr;
+  TransactionLogData *m_data = nullptr;
 
   Arena m_arena;
 
diff --git a/include/proxy/logging/TransactionLogData.h 
b/include/proxy/logging/TransactionLogData.h
new file mode 100644
index 0000000000..55ae45150e
--- /dev/null
+++ b/include/proxy/logging/TransactionLogData.h
@@ -0,0 +1,607 @@
+/** @file
+
+  Base class providing the data interface for access log entries.
+
+  @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 "proxy/Milestones.h"
+#include "proxy/hdrs/HTTP.h"
+#include "tscore/ink_inet.h"
+
+#include <optional>
+#include <string_view>
+
+class HTTPHdr;
+
+/** Abstract base for the data backing a single access log entry.
+ *
+ * Subclasses provide data from the appropriate source via virtual getters:
+ *   - CompletedTransactionLogData reads from HttpSM (defined in the http
+ *     module) for transactions that completed normally.
+ *   - PreTransactionLogData returns owned storage (defined in the proxy
+ *     module), for requests that never create an HttpSM.
+ *
+ * LogAccess reads only from this interface, so the logging module has no
+ * compile-time dependency on the http module.
+ */
+class TransactionLogData
+{
+public:
+  virtual ~TransactionLogData() = default;
+
+  /** Return the HttpSM pointer for plugin custom marshal functions.
+   *
+   * Only TransactionLogData provides a non-null value.  Pre-transaction
+   * entries have no HttpSM, so plugins receive nullptr.
+   *
+   * @return An opaque pointer to the HttpSM, or nullptr.
+   */
+  virtual void *
+  http_sm_for_plugins() const
+  {
+    return nullptr;
+  }
+
+  // ===== Milestones =====
+
+  virtual TransactionMilestones const *
+  get_milestones() const
+  {
+    return nullptr;
+  }
+
+  // ===== Headers =====
+
+  virtual HTTPHdr *
+  get_client_request() const
+  {
+    return nullptr;
+  }
+  virtual HTTPHdr *
+  get_proxy_response() const
+  {
+    return nullptr;
+  }
+  virtual HTTPHdr *
+  get_proxy_request() const
+  {
+    return nullptr;
+  }
+  virtual HTTPHdr *
+  get_server_response() const
+  {
+    return nullptr;
+  }
+  virtual HTTPHdr *
+  get_cache_response() const
+  {
+    return nullptr;
+  }
+
+  // ===== Client request URL / path =====
+
+  virtual const char *
+  get_client_req_url_str() const
+  {
+    return nullptr;
+  }
+  virtual int
+  get_client_req_url_len() const
+  {
+    return 0;
+  }
+  virtual const char *
+  get_client_req_url_path_str() const
+  {
+    return nullptr;
+  }
+  virtual int
+  get_client_req_url_path_len() const
+  {
+    return 0;
+  }
+
+  // ===== Proxy response content-type / reason =====
+
+  virtual char *
+  get_proxy_resp_content_type_str() const
+  {
+    return nullptr;
+  }
+  virtual int
+  get_proxy_resp_content_type_len() const
+  {
+    return 0;
+  }
+  virtual char *
+  get_proxy_resp_reason_phrase_str() const
+  {
+    return nullptr;
+  }
+  virtual int
+  get_proxy_resp_reason_phrase_len() const
+  {
+    return 0;
+  }
+
+  // ===== Unmapped URL =====
+
+  virtual char *
+  get_unmapped_url_str() const
+  {
+    return nullptr;
+  }
+  virtual int
+  get_unmapped_url_len() const
+  {
+    return 0;
+  }
+
+  // ===== Cache lookup URL =====
+
+  virtual char *
+  get_cache_lookup_url_str() const
+  {
+    return nullptr;
+  }
+  virtual int
+  get_cache_lookup_url_len() const
+  {
+    return 0;
+  }
+
+  // ===== Client addressing =====
+
+  virtual sockaddr const *
+  get_client_addr() const
+  {
+    return nullptr;
+  }
+  virtual sockaddr const *
+  get_client_src_addr() const
+  {
+    return nullptr;
+  }
+  virtual sockaddr const *
+  get_client_dst_addr() const
+  {
+    return nullptr;
+  }
+  virtual sockaddr const *
+  get_verified_client_addr() const
+  {
+    return nullptr;
+  }
+  virtual uint16_t
+  get_client_port() const
+  {
+    return 0;
+  }
+
+  // ===== Server addressing =====
+
+  virtual sockaddr const *
+  get_server_src_addr() const
+  {
+    return nullptr;
+  }
+  virtual sockaddr const *
+  get_server_dst_addr() const
+  {
+    return nullptr;
+  }
+  virtual sockaddr const *
+  get_server_info_dst_addr() const
+  {
+    return nullptr;
+  }
+  virtual const char *
+  get_server_name() const
+  {
+    return nullptr;
+  }
+
+  // ===== Squid codes =====
+
+  virtual SquidLogCode
+  get_log_code() const
+  {
+    return SquidLogCode::EMPTY;
+  }
+  virtual SquidSubcode
+  get_subcode() const
+  {
+    return SquidSubcode::EMPTY;
+  }
+  virtual SquidHitMissCode
+  get_hit_miss_code() const
+  {
+    return SQUID_MISS_NONE;
+  }
+  virtual SquidHierarchyCode
+  get_hier_code() const
+  {
+    return SquidHierarchyCode::NONE;
+  }
+
+  // ===== Byte counters =====
+
+  virtual int64_t
+  get_client_request_body_bytes() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_client_response_hdr_bytes() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_client_response_body_bytes() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_server_request_body_bytes() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_server_response_body_bytes() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_cache_response_body_bytes() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_cache_response_hdr_bytes() const
+  {
+    return 0;
+  }
+
+  // ===== Transaction identifiers =====
+
+  virtual int64_t
+  get_sm_id() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_connection_id() const
+  {
+    return 0;
+  }
+  virtual int
+  get_transaction_id() const
+  {
+    return 0;
+  }
+  virtual int
+  get_transaction_priority_weight() const
+  {
+    return 0;
+  }
+  virtual int
+  get_transaction_priority_dependence() const
+  {
+    return 0;
+  }
+
+  // ===== Plugin info =====
+
+  virtual int64_t
+  get_plugin_id() const
+  {
+    return 0;
+  }
+  virtual const char *
+  get_plugin_tag() const
+  {
+    return nullptr;
+  }
+
+  // ===== Protocol info =====
+
+  virtual const char *
+  get_client_protocol() const
+  {
+    return nullptr;
+  }
+  virtual const char *
+  get_server_protocol() const
+  {
+    return nullptr;
+  }
+  virtual const char *
+  get_client_sec_protocol() const
+  {
+    return nullptr;
+  }
+  virtual const char *
+  get_client_cipher_suite() const
+  {
+    return nullptr;
+  }
+  virtual const char *
+  get_client_curve() const
+  {
+    return nullptr;
+  }
+  virtual const char *
+  get_client_security_group() const
+  {
+    return nullptr;
+  }
+  virtual int
+  get_client_alpn_id() const
+  {
+    return -1;
+  }
+
+  // ===== SNI =====
+
+  virtual const char *
+  get_sni_server_name() const
+  {
+    return nullptr;
+  }
+
+  // ===== Connection flags =====
+
+  virtual bool
+  get_client_tcp_reused() const
+  {
+    return false;
+  }
+  virtual bool
+  get_client_connection_is_ssl() const
+  {
+    return false;
+  }
+  virtual bool
+  get_client_ssl_reused() const
+  {
+    return false;
+  }
+  /** Get the client TLS session resumption type (full handshake, session ID, 
or session ticket).
+
+      @return The HttpUserAgent client SSL resumption type integer.
+  */
+  virtual int
+  get_client_ssl_resumption_type() const
+  {
+    return 0;
+  }
+  virtual bool
+  get_is_internal() const
+  {
+    return false;
+  }
+  virtual bool
+  get_server_connection_is_ssl() const
+  {
+    return false;
+  }
+  virtual bool
+  get_server_ssl_reused() const
+  {
+    return false;
+  }
+  virtual int
+  get_server_connection_provided_cert() const
+  {
+    return 0;
+  }
+  virtual int
+  get_client_provided_cert() const
+  {
+    return 0;
+  }
+
+  // ===== Server transaction count =====
+
+  virtual int64_t
+  get_server_transact_count() const
+  {
+    return 0;
+  }
+
+  // ===== Finish status =====
+
+  virtual int
+  get_client_finish_status_code() const
+  {
+    return 0;
+  }
+  virtual int
+  get_proxy_finish_status_code() const
+  {
+    return 0;
+  }
+
+  // ===== Error codes =====
+
+  virtual const char *
+  get_client_rx_error_code() const
+  {
+    return "-";
+  }
+  virtual const char *
+  get_client_tx_error_code() const
+  {
+    return "-";
+  }
+
+  // ===== MPTCP =====
+
+  virtual std::optional<bool>
+  get_mptcp_state() const
+  {
+    return std::nullopt;
+  }
+
+  // ===== Misc transaction state =====
+
+  virtual in_port_t
+  get_incoming_port() const
+  {
+    return 0;
+  }
+  virtual int
+  get_orig_scheme() const
+  {
+    return -1;
+  }
+  virtual int64_t
+  get_congestion_control_crat() const
+  {
+    return 0;
+  }
+
+  // ===== Cache state =====
+
+  virtual int
+  get_cache_write_code() const
+  {
+    return 0;
+  }
+  virtual int
+  get_cache_transform_write_code() const
+  {
+    return 0;
+  }
+  virtual int
+  get_cache_open_read_tries() const
+  {
+    return 0;
+  }
+  virtual int
+  get_cache_open_write_tries() const
+  {
+    return 0;
+  }
+  virtual int
+  get_max_cache_open_write_retries() const
+  {
+    return -1;
+  }
+
+  // ===== Retry attempts =====
+
+  virtual int64_t
+  get_simple_retry_attempts() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_unavailable_retry_attempts() const
+  {
+    return 0;
+  }
+  virtual int64_t
+  get_retry_attempts_saved() const
+  {
+    return 0;
+  }
+
+  // ===== Status plugin entry name =====
+
+  virtual std::string_view
+  get_http_return_code_setter_name() const
+  {
+    return {};
+  }
+
+  // ===== Proxy Protocol =====
+
+  virtual int
+  get_pp_version() const
+  {
+    return 0;
+  }
+  virtual sockaddr const *
+  get_pp_src_addr() const
+  {
+    return nullptr;
+  }
+  virtual sockaddr const *
+  get_pp_dst_addr() const
+  {
+    return nullptr;
+  }
+  virtual std::string_view
+  get_pp_authority() const
+  {
+    return {};
+  }
+  virtual std::string_view
+  get_pp_tls_cipher() const
+  {
+    return {};
+  }
+  virtual std::string_view
+  get_pp_tls_version() const
+  {
+    return {};
+  }
+  virtual std::string_view
+  get_pp_tls_group() const
+  {
+    return {};
+  }
+
+  // ===== Server response Transfer-Encoding =====
+
+  virtual std::string_view
+  get_server_response_transfer_encoding() const
+  {
+    return {};
+  }
+
+  // ===== Fallback fields for pre-transaction logging =====
+
+  virtual std::string_view
+  get_method() const
+  {
+    return {};
+  }
+  virtual std::string_view
+  get_scheme() const
+  {
+    return {};
+  }
+  virtual std::string_view
+  get_client_protocol_str() const
+  {
+    return {};
+  }
+
+  // noncopyable
+  TransactionLogData(const TransactionLogData &)            = delete;
+  TransactionLogData &operator=(const TransactionLogData &) = delete;
+
+protected:
+  TransactionLogData() = default;
+};
diff --git a/src/proxy/ProxyTransaction.cc b/src/proxy/ProxyTransaction.cc
index 3d0bea2008..ee02429722 100644
--- a/src/proxy/ProxyTransaction.cc
+++ b/src/proxy/ProxyTransaction.cc
@@ -23,6 +23,9 @@
 
 #include "proxy/http/HttpSM.h"
 #include "proxy/Plugin.h"
+#include "proxy/PreTransactionLogData.h"
+#include "proxy/logging/LogAccess.h"
+#include "proxy/logging/Log.h"
 
 namespace
 {
@@ -290,3 +293,124 @@ ProxyTransaction::mark_as_tunnel_endpoint()
   ink_assert(nvc != nullptr);
   nvc->mark_as_tunnel_endpoint();
 }
+
+namespace
+{
+/** Build a best-effort request target for access logging. */
+std::string
+synthesize_request_target(std::string_view method, std::string_view scheme, 
std::string_view authority, std::string_view path)
+{
+  if (method == static_cast<std::string_view>(HTTP_METHOD_CONNECT)) {
+    if (!authority.empty()) {
+      return std::string(authority);
+    }
+    if (!path.empty()) {
+      return std::string(path);
+    }
+    return {};
+  }
+
+  if (!scheme.empty() && !authority.empty()) {
+    std::string url;
+    url.reserve(scheme.size() + authority.size() + path.size() + 4);
+    url.append(scheme);
+    url.append("://");
+    url.append(authority);
+    if (!path.empty()) {
+      url.append(path);
+    } else {
+      url.push_back('/');
+    }
+    return url;
+  }
+
+  if (!path.empty()) {
+    return std::string(path);
+  }
+
+  return authority.empty() ? std::string{} : std::string(authority);
+}
+
+std::string_view
+get_pseudo_header_value(HTTPHdr const &hdr, std::string_view name)
+{
+  if (auto const *field = hdr.field_find(name); field != nullptr) {
+    return field->value_get();
+  }
+  return {};
+}
+} // end anonymous namespace
+
+void
+ProxyTransaction::log_pre_transaction_access(HTTPHdr const *request, const 
char *protocol_str)
+{
+  if (get_sm() != nullptr) {
+    return;
+  }
+
+  if (request == nullptr || !request->valid() || request->type_get() != 
HTTPType::REQUEST) {
+    return;
+  }
+
+  ProxySession *ssn = get_proxy_ssn();
+  if (ssn == nullptr) {
+    return;
+  }
+
+  PreTransactionLogData data;
+
+  data.owned_client_request.create(HTTPType::REQUEST, request->version_get());
+  data.owned_client_request.copy(request);
+  data.m_client_connection_is_ssl = ssn->ssl() != nullptr;
+
+  auto const method_sv    = get_pseudo_header_value(*request, 
PSEUDO_HEADER_METHOD);
+  auto const scheme_sv    = get_pseudo_header_value(*request, 
PSEUDO_HEADER_SCHEME);
+  auto const authority_sv = get_pseudo_header_value(*request, 
PSEUDO_HEADER_AUTHORITY);
+  auto const path_sv      = get_pseudo_header_value(*request, 
PSEUDO_HEADER_PATH);
+
+  if (!method_sv.empty()) {
+    data.owned_method.assign(method_sv.data(), method_sv.size());
+  } else {
+    auto const mget = const_cast<HTTPHdr *>(request)->method_get();
+    if (!mget.empty()) {
+      data.owned_method.assign(mget.data(), mget.size());
+    }
+  }
+
+  if (!scheme_sv.empty()) {
+    data.owned_scheme.assign(scheme_sv.data(), scheme_sv.size());
+  }
+  if (!authority_sv.empty()) {
+    data.owned_authority.assign(authority_sv.data(), authority_sv.size());
+  }
+  if (!path_sv.empty()) {
+    data.owned_path.assign(path_sv.data(), path_sv.size());
+  }
+  data.owned_url = synthesize_request_target(data.owned_method, 
data.owned_scheme, data.owned_authority, data.owned_path);
+
+  if (protocol_str) {
+    data.owned_client_protocol_str = protocol_str;
+  }
+
+  ats_ip_copy(&data.owned_client_addr.sa, ssn->get_remote_addr());
+  ats_ip_copy(&data.owned_client_src_addr.sa, ssn->get_remote_addr());
+  ats_ip_copy(&data.owned_client_dst_addr.sa, ssn->get_local_addr());
+  data.m_client_port = ats_ip_port_host_order(ssn->get_remote_addr());
+
+  data.m_connection_id  = ssn->connection_id();
+  data.m_transaction_id = get_transaction_id();
+
+  data.m_log_code      = SquidLogCode::ERR_INVALID_REQ;
+  data.m_hit_miss_code = SQUID_MISS_NONE;
+  data.m_hier_code     = SquidHierarchyCode::NONE;
+
+  ink_hrtime const now                                    = ink_get_hrtime();
+  data.owned_milestones[TS_MILESTONE_SM_START]            = now;
+  data.owned_milestones[TS_MILESTONE_UA_BEGIN]            = now;
+  data.owned_milestones[TS_MILESTONE_UA_FIRST_READ]       = now;
+  data.owned_milestones[TS_MILESTONE_UA_READ_HEADER_DONE] = now;
+  data.owned_milestones[TS_MILESTONE_SM_FINISH]           = now;
+
+  LogAccess access(data);
+  Log::access(&access);
+}
diff --git a/src/proxy/http/CMakeLists.txt b/src/proxy/http/CMakeLists.txt
index 55cf7ebd36..94a3130803 100644
--- a/src/proxy/http/CMakeLists.txt
+++ b/src/proxy/http/CMakeLists.txt
@@ -27,6 +27,7 @@ add_library(
   HttpDebugNames.cc
   HttpProxyServerMain.cc
   HttpSM.cc
+  CompletedTransactionLogData.cc
   Http1ServerSession.cc
   HttpSessionManager.cc
   HttpTransact.cc
diff --git a/src/proxy/http/CompletedTransactionLogData.cc 
b/src/proxy/http/CompletedTransactionLogData.cc
new file mode 100644
index 0000000000..4c8f418819
--- /dev/null
+++ b/src/proxy/http/CompletedTransactionLogData.cc
@@ -0,0 +1,832 @@
+/** @file
+
+  CompletedTransactionLogData implementation: read TransactionLogData from a
+  live HttpSM.
+
+  @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 "proxy/http/CompletedTransactionLogData.h"
+#include "proxy/http/HttpSM.h"
+#include "proxy/logging/LogAccess.h"
+#include "proxy/hdrs/MIME.h"
+#include "../private/SSLProxySession.h"
+
+namespace
+{
+/** Map HttpTransact::CacheWriteStatus_t to LogCacheWriteCodeType. */
+int
+convert_cache_write_code(HttpTransact::CacheWriteStatus_t t)
+{
+  switch (t) {
+  case HttpTransact::CacheWriteStatus_t::NO_WRITE:
+    return LOG_CACHE_WRITE_NONE;
+  case HttpTransact::CacheWriteStatus_t::LOCK_MISS:
+    return LOG_CACHE_WRITE_LOCK_MISSED;
+  case HttpTransact::CacheWriteStatus_t::IN_PROGRESS:
+    return LOG_CACHE_WRITE_LOCK_ABORTED;
+  case HttpTransact::CacheWriteStatus_t::ERROR:
+    return LOG_CACHE_WRITE_ERROR;
+  case HttpTransact::CacheWriteStatus_t::COMPLETE:
+    return LOG_CACHE_WRITE_COMPLETE;
+  default:
+    ink_assert(!"bad cache write code");
+    return LOG_CACHE_WRITE_NONE;
+  }
+}
+
+int
+compute_client_finish_status(HttpSM *sm)
+{
+  HttpTransact::AbortState_t cl_abort_state = sm->t_state.client_info.abort;
+  if (cl_abort_state == HttpTransact::ABORTED) {
+    if (sm->t_state.client_info.state == HttpTransact::ACTIVE_TIMEOUT ||
+        sm->t_state.client_info.state == HttpTransact::INACTIVE_TIMEOUT) {
+      return LOG_FINISH_TIMEOUT;
+    }
+    return LOG_FINISH_INTR;
+  }
+  return LOG_FINISH_FIN;
+}
+
+int
+compute_proxy_finish_status(HttpSM *sm)
+{
+  if (sm->t_state.current.server) {
+    switch (sm->t_state.current.server->state) {
+    case HttpTransact::ACTIVE_TIMEOUT:
+    case HttpTransact::INACTIVE_TIMEOUT:
+      return LOG_FINISH_TIMEOUT;
+    case HttpTransact::CONNECTION_ERROR:
+      return LOG_FINISH_INTR;
+    default:
+      if (sm->t_state.current.server->abort == HttpTransact::ABORTED) {
+        return LOG_FINISH_INTR;
+      }
+      break;
+    }
+  }
+  return LOG_FINISH_FIN;
+}
+} // end anonymous namespace
+
+CompletedTransactionLogData::CompletedTransactionLogData(HttpSM *sm) : 
m_http_sm(sm)
+{
+  ink_assert(sm != nullptr);
+}
+
+void *
+CompletedTransactionLogData::http_sm_for_plugins() const
+{
+  return m_http_sm;
+}
+
+// ===== Milestones =====
+
+TransactionMilestones const *
+CompletedTransactionLogData::get_milestones() const
+{
+  return &m_http_sm->milestones;
+}
+
+// ===== Headers =====
+
+HTTPHdr *
+CompletedTransactionLogData::get_client_request() const
+{
+  HttpTransact::HeaderInfo *hdr = &m_http_sm->t_state.hdr_info;
+  if (hdr->client_request.valid()) {
+    return &hdr->client_request;
+  }
+  return nullptr;
+}
+
+HTTPHdr *
+CompletedTransactionLogData::get_proxy_response() const
+{
+  HttpTransact::HeaderInfo *hdr = &m_http_sm->t_state.hdr_info;
+  if (hdr->client_response.valid()) {
+    return &hdr->client_response;
+  }
+  return nullptr;
+}
+
+HTTPHdr *
+CompletedTransactionLogData::get_proxy_request() const
+{
+  HttpTransact::HeaderInfo *hdr = &m_http_sm->t_state.hdr_info;
+  if (hdr->server_request.valid()) {
+    return &hdr->server_request;
+  }
+  return nullptr;
+}
+
+HTTPHdr *
+CompletedTransactionLogData::get_server_response() const
+{
+  HttpTransact::HeaderInfo *hdr = &m_http_sm->t_state.hdr_info;
+  if (hdr->server_response.valid()) {
+    return &hdr->server_response;
+  }
+  return nullptr;
+}
+
+HTTPHdr *
+CompletedTransactionLogData::get_cache_response() const
+{
+  HttpTransact::HeaderInfo *hdr = &m_http_sm->t_state.hdr_info;
+  if (hdr->cache_response.valid()) {
+    return &hdr->cache_response;
+  }
+  return nullptr;
+}
+
+// ===== Client request URL / path =====
+
+void
+CompletedTransactionLogData::cache_url_strings() const
+{
+  if (m_url_cached) {
+    return;
+  }
+  m_url_cached = true;
+
+  HTTPHdr *client_request = get_client_request();
+  if (client_request) {
+    m_client_req_url_str      = 
client_request->url_string_get_ref(&m_client_req_url_len);
+    auto path_sv              = client_request->path_get();
+    m_client_req_url_path_str = path_sv.data();
+    m_client_req_url_path_len = static_cast<int>(path_sv.length());
+  }
+}
+
+const char *
+CompletedTransactionLogData::get_client_req_url_str() const
+{
+  cache_url_strings();
+  return m_client_req_url_str;
+}
+
+int
+CompletedTransactionLogData::get_client_req_url_len() const
+{
+  cache_url_strings();
+  return m_client_req_url_len;
+}
+
+const char *
+CompletedTransactionLogData::get_client_req_url_path_str() const
+{
+  cache_url_strings();
+  return m_client_req_url_path_str;
+}
+
+int
+CompletedTransactionLogData::get_client_req_url_path_len() const
+{
+  cache_url_strings();
+  return m_client_req_url_path_len;
+}
+
+// ===== Proxy response content-type / reason =====
+
+void
+CompletedTransactionLogData::cache_content_type() const
+{
+  if (m_content_type_cached) {
+    return;
+  }
+  m_content_type_cached = true;
+
+  HTTPHdr *proxy_response = get_proxy_response();
+  if (proxy_response) {
+    MIMEField *field = 
proxy_response->field_find(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE));
+    if (field) {
+      auto ct                       = field->value_get();
+      m_proxy_resp_content_type_str = const_cast<char *>(ct.data());
+      m_proxy_resp_content_type_len = ct.length();
+    } else {
+      constexpr std::string_view hidden_ct{"@Content-Type"};
+      field = proxy_response->field_find(hidden_ct);
+      if (field) {
+        auto ct                       = field->value_get();
+        m_proxy_resp_content_type_str = const_cast<char *>(ct.data());
+        m_proxy_resp_content_type_len = ct.length();
+      }
+    }
+    auto reason                    = proxy_response->reason_get();
+    m_proxy_resp_reason_phrase_str = const_cast<char *>(reason.data());
+    m_proxy_resp_reason_phrase_len = static_cast<int>(reason.length());
+  }
+}
+
+char *
+CompletedTransactionLogData::get_proxy_resp_content_type_str() const
+{
+  cache_content_type();
+  return m_proxy_resp_content_type_str;
+}
+
+int
+CompletedTransactionLogData::get_proxy_resp_content_type_len() const
+{
+  cache_content_type();
+  return m_proxy_resp_content_type_len;
+}
+
+char *
+CompletedTransactionLogData::get_proxy_resp_reason_phrase_str() const
+{
+  cache_content_type();
+  return m_proxy_resp_reason_phrase_str;
+}
+
+int
+CompletedTransactionLogData::get_proxy_resp_reason_phrase_len() const
+{
+  cache_content_type();
+  return m_proxy_resp_reason_phrase_len;
+}
+
+// ===== Unmapped URL =====
+
+char *
+CompletedTransactionLogData::get_unmapped_url_str() const
+{
+  if (m_http_sm->t_state.unmapped_url.valid()) {
+    int len = 0;
+    return m_http_sm->t_state.unmapped_url.string_get_ref(&len);
+  }
+  return nullptr;
+}
+
+int
+CompletedTransactionLogData::get_unmapped_url_len() const
+{
+  if (m_http_sm->t_state.unmapped_url.valid()) {
+    int len = 0;
+    m_http_sm->t_state.unmapped_url.string_get_ref(&len);
+    return len;
+  }
+  return 0;
+}
+
+// ===== Cache lookup URL =====
+
+char *
+CompletedTransactionLogData::get_cache_lookup_url_str() const
+{
+  if (m_http_sm->t_state.cache_info.lookup_url_storage.valid()) {
+    int len = 0;
+    return 
m_http_sm->t_state.cache_info.lookup_url_storage.string_get_ref(&len);
+  }
+  return nullptr;
+}
+
+int
+CompletedTransactionLogData::get_cache_lookup_url_len() const
+{
+  if (m_http_sm->t_state.cache_info.lookup_url_storage.valid()) {
+    int len = 0;
+    m_http_sm->t_state.cache_info.lookup_url_storage.string_get_ref(&len);
+    return len;
+  }
+  return 0;
+}
+
+// ===== Client addressing =====
+
+sockaddr const *
+CompletedTransactionLogData::get_client_addr() const
+{
+  return &m_http_sm->t_state.effective_client_addr.sa;
+}
+
+sockaddr const *
+CompletedTransactionLogData::get_client_src_addr() const
+{
+  return &m_http_sm->t_state.client_info.src_addr.sa;
+}
+
+sockaddr const *
+CompletedTransactionLogData::get_client_dst_addr() const
+{
+  return &m_http_sm->t_state.client_info.dst_addr.sa;
+}
+
+sockaddr const *
+CompletedTransactionLogData::get_verified_client_addr() const
+{
+  if (auto txn = m_http_sm->get_ua_txn(); txn) {
+    sockaddr const *vaddr = txn->get_verified_client_addr();
+    if (vaddr && ats_is_ip(vaddr)) {
+      return vaddr;
+    }
+  }
+  return nullptr;
+}
+
+uint16_t
+CompletedTransactionLogData::get_client_port() const
+{
+  if (auto txn = m_http_sm->get_ua_txn(); txn) {
+    return txn->get_client_port();
+  }
+  return 0;
+}
+
+// ===== Server addressing =====
+
+sockaddr const *
+CompletedTransactionLogData::get_server_src_addr() const
+{
+  if (m_http_sm->t_state.current.server) {
+    return &m_http_sm->t_state.current.server->src_addr.sa;
+  }
+  return nullptr;
+}
+
+sockaddr const *
+CompletedTransactionLogData::get_server_dst_addr() const
+{
+  if (m_http_sm->t_state.current.server) {
+    return &m_http_sm->t_state.current.server->dst_addr.sa;
+  }
+  return nullptr;
+}
+
+sockaddr const *
+CompletedTransactionLogData::get_server_info_dst_addr() const
+{
+  return &m_http_sm->t_state.server_info.dst_addr.sa;
+}
+
+const char *
+CompletedTransactionLogData::get_server_name() const
+{
+  if (m_http_sm->t_state.current.server) {
+    return m_http_sm->t_state.current.server->name;
+  }
+  return nullptr;
+}
+
+// ===== Squid codes =====
+
+SquidLogCode
+CompletedTransactionLogData::get_log_code() const
+{
+  return m_http_sm->t_state.squid_codes.log_code;
+}
+
+SquidSubcode
+CompletedTransactionLogData::get_subcode() const
+{
+  return m_http_sm->t_state.squid_codes.subcode;
+}
+
+SquidHitMissCode
+CompletedTransactionLogData::get_hit_miss_code() const
+{
+  return m_http_sm->t_state.squid_codes.hit_miss_code;
+}
+
+SquidHierarchyCode
+CompletedTransactionLogData::get_hier_code() const
+{
+  return m_http_sm->t_state.squid_codes.hier_code;
+}
+
+// ===== Byte counters =====
+
+int64_t
+CompletedTransactionLogData::get_client_request_body_bytes() const
+{
+  return m_http_sm->client_request_body_bytes;
+}
+
+int64_t
+CompletedTransactionLogData::get_client_response_hdr_bytes() const
+{
+  return m_http_sm->client_response_hdr_bytes;
+}
+
+int64_t
+CompletedTransactionLogData::get_client_response_body_bytes() const
+{
+  return m_http_sm->client_response_body_bytes;
+}
+
+int64_t
+CompletedTransactionLogData::get_server_request_body_bytes() const
+{
+  return m_http_sm->server_request_body_bytes;
+}
+
+int64_t
+CompletedTransactionLogData::get_server_response_body_bytes() const
+{
+  return m_http_sm->server_response_body_bytes;
+}
+
+int64_t
+CompletedTransactionLogData::get_cache_response_body_bytes() const
+{
+  return m_http_sm->cache_response_body_bytes;
+}
+
+int64_t
+CompletedTransactionLogData::get_cache_response_hdr_bytes() const
+{
+  return m_http_sm->cache_response_hdr_bytes;
+}
+
+// ===== Transaction identifiers =====
+
+int64_t
+CompletedTransactionLogData::get_sm_id() const
+{
+  return m_http_sm->sm_id;
+}
+
+int64_t
+CompletedTransactionLogData::get_connection_id() const
+{
+  return m_http_sm->client_connection_id();
+}
+
+int
+CompletedTransactionLogData::get_transaction_id() const
+{
+  return m_http_sm->client_transaction_id();
+}
+
+int
+CompletedTransactionLogData::get_transaction_priority_weight() const
+{
+  return m_http_sm->client_transaction_priority_weight();
+}
+
+int
+CompletedTransactionLogData::get_transaction_priority_dependence() const
+{
+  return m_http_sm->client_transaction_priority_dependence();
+}
+
+// ===== Plugin info =====
+
+int64_t
+CompletedTransactionLogData::get_plugin_id() const
+{
+  return m_http_sm->plugin_id;
+}
+
+const char *
+CompletedTransactionLogData::get_plugin_tag() const
+{
+  return m_http_sm->plugin_tag;
+}
+
+// ===== Protocol info =====
+
+const char *
+CompletedTransactionLogData::get_client_protocol() const
+{
+  return m_http_sm->get_user_agent().get_client_protocol();
+}
+
+const char *
+CompletedTransactionLogData::get_server_protocol() const
+{
+  return m_http_sm->server_protocol;
+}
+
+const char *
+CompletedTransactionLogData::get_client_sec_protocol() const
+{
+  return m_http_sm->get_user_agent().get_client_sec_protocol();
+}
+
+const char *
+CompletedTransactionLogData::get_client_cipher_suite() const
+{
+  return m_http_sm->get_user_agent().get_client_cipher_suite();
+}
+
+const char *
+CompletedTransactionLogData::get_client_curve() const
+{
+  return m_http_sm->get_user_agent().get_client_curve();
+}
+
+const char *
+CompletedTransactionLogData::get_client_security_group() const
+{
+  return m_http_sm->get_user_agent().get_client_security_group();
+}
+
+int
+CompletedTransactionLogData::get_client_alpn_id() const
+{
+  return m_http_sm->get_user_agent().get_client_alpn_id();
+}
+
+// ===== SNI =====
+
+const char *
+CompletedTransactionLogData::get_sni_server_name() const
+{
+  if (auto txn = m_http_sm->get_ua_txn(); txn) {
+    if (auto ssn = txn->get_proxy_ssn(); ssn) {
+      if (auto ssl = ssn->ssl(); ssl) {
+        return ssl->client_sni_server_name();
+      }
+    }
+  }
+  return nullptr;
+}
+
+// ===== Connection flags =====
+
+bool
+CompletedTransactionLogData::get_client_tcp_reused() const
+{
+  return m_http_sm->get_user_agent().get_client_tcp_reused();
+}
+
+bool
+CompletedTransactionLogData::get_client_connection_is_ssl() const
+{
+  return m_http_sm->get_user_agent().get_client_connection_is_ssl();
+}
+
+bool
+CompletedTransactionLogData::get_client_ssl_reused() const
+{
+  return m_http_sm->get_user_agent().get_client_ssl_reused();
+}
+
+int
+CompletedTransactionLogData::get_client_ssl_resumption_type() const
+{
+  return m_http_sm->get_user_agent().get_client_ssl_resumption_type();
+}
+
+bool
+CompletedTransactionLogData::get_is_internal() const
+{
+  return m_http_sm->is_internal;
+}
+
+bool
+CompletedTransactionLogData::get_server_connection_is_ssl() const
+{
+  return m_http_sm->server_connection_is_ssl;
+}
+
+bool
+CompletedTransactionLogData::get_server_ssl_reused() const
+{
+  return m_http_sm->server_ssl_reused;
+}
+
+int
+CompletedTransactionLogData::get_server_connection_provided_cert() const
+{
+  return m_http_sm->server_connection_provided_cert;
+}
+
+int
+CompletedTransactionLogData::get_client_provided_cert() const
+{
+  if (auto txn = m_http_sm->get_ua_txn(); txn) {
+    if (auto ssn = txn->get_proxy_ssn(); ssn) {
+      if (auto ssl = ssn->ssl(); ssl) {
+        return ssl->client_provided_certificate();
+      }
+    }
+  }
+  return 0;
+}
+
+// ===== Server transaction count =====
+
+int64_t
+CompletedTransactionLogData::get_server_transact_count() const
+{
+  return m_http_sm->server_transact_count;
+}
+
+// ===== Finish status =====
+
+int
+CompletedTransactionLogData::get_client_finish_status_code() const
+{
+  return compute_client_finish_status(m_http_sm);
+}
+
+int
+CompletedTransactionLogData::get_proxy_finish_status_code() const
+{
+  return compute_proxy_finish_status(m_http_sm);
+}
+
+// ===== Error codes =====
+
+void
+CompletedTransactionLogData::format_error_codes() const
+{
+  if (m_error_codes_formatted) {
+    return;
+  }
+  m_error_codes_formatted = true;
+  m_http_sm->t_state.client_info.rx_error_code.str(m_client_rx_error_code, 
sizeof(m_client_rx_error_code));
+  m_http_sm->t_state.client_info.tx_error_code.str(m_client_tx_error_code, 
sizeof(m_client_tx_error_code));
+}
+
+const char *
+CompletedTransactionLogData::get_client_rx_error_code() const
+{
+  format_error_codes();
+  return m_client_rx_error_code;
+}
+
+const char *
+CompletedTransactionLogData::get_client_tx_error_code() const
+{
+  format_error_codes();
+  return m_client_tx_error_code;
+}
+
+// ===== MPTCP =====
+
+std::optional<bool>
+CompletedTransactionLogData::get_mptcp_state() const
+{
+  return m_http_sm->mptcp_state;
+}
+
+// ===== Misc transaction state =====
+
+in_port_t
+CompletedTransactionLogData::get_incoming_port() const
+{
+  return m_http_sm->t_state.request_data.incoming_port;
+}
+
+int
+CompletedTransactionLogData::get_orig_scheme() const
+{
+  return m_http_sm->t_state.orig_scheme;
+}
+
+int64_t
+CompletedTransactionLogData::get_congestion_control_crat() const
+{
+  return m_http_sm->t_state.congestion_control_crat;
+}
+
+// ===== Cache state =====
+
+int
+CompletedTransactionLogData::get_cache_write_code() const
+{
+  return convert_cache_write_code(m_http_sm->t_state.cache_info.write_status);
+}
+
+int
+CompletedTransactionLogData::get_cache_transform_write_code() const
+{
+  return 
convert_cache_write_code(m_http_sm->t_state.cache_info.transform_write_status);
+}
+
+int
+CompletedTransactionLogData::get_cache_open_read_tries() const
+{
+  return m_http_sm->get_cache_sm().get_open_read_tries();
+}
+
+int
+CompletedTransactionLogData::get_cache_open_write_tries() const
+{
+  return m_http_sm->get_cache_sm().get_open_write_tries();
+}
+
+int
+CompletedTransactionLogData::get_max_cache_open_write_retries() const
+{
+  return m_http_sm->t_state.txn_conf->max_cache_open_write_retries;
+}
+
+// ===== Retry attempts =====
+
+int64_t
+CompletedTransactionLogData::get_simple_retry_attempts() const
+{
+  return m_http_sm->t_state.current.simple_retry_attempts;
+}
+
+int64_t
+CompletedTransactionLogData::get_unavailable_retry_attempts() const
+{
+  return m_http_sm->t_state.current.unavailable_server_retry_attempts;
+}
+
+int64_t
+CompletedTransactionLogData::get_retry_attempts_saved() const
+{
+  return m_http_sm->t_state.current.retry_attempts.saved();
+}
+
+// ===== Status plugin entry name =====
+
+std::string_view
+CompletedTransactionLogData::get_http_return_code_setter_name() const
+{
+  return m_http_sm->t_state.http_return_code_setter_name;
+}
+
+// ===== Proxy Protocol =====
+
+int
+CompletedTransactionLogData::get_pp_version() const
+{
+  if (m_http_sm->t_state.pp_info.version != ProxyProtocolVersion::UNDEFINED) {
+    return static_cast<int>(m_http_sm->t_state.pp_info.version);
+  }
+  return 0;
+}
+
+sockaddr const *
+CompletedTransactionLogData::get_pp_src_addr() const
+{
+  if (m_http_sm->t_state.pp_info.version != ProxyProtocolVersion::UNDEFINED) {
+    return &m_http_sm->t_state.pp_info.src_addr.sa;
+  }
+  return nullptr;
+}
+
+sockaddr const *
+CompletedTransactionLogData::get_pp_dst_addr() const
+{
+  if (m_http_sm->t_state.pp_info.version != ProxyProtocolVersion::UNDEFINED) {
+    return &m_http_sm->t_state.pp_info.dst_addr.sa;
+  }
+  return nullptr;
+}
+
+std::string_view
+CompletedTransactionLogData::get_pp_authority() const
+{
+  if (m_http_sm->t_state.pp_info.version != ProxyProtocolVersion::UNDEFINED) {
+    if (auto authority_opt = 
m_http_sm->t_state.pp_info.get_tlv(PP2_TYPE_AUTHORITY); authority_opt) {
+      return *authority_opt;
+    }
+  }
+  return {};
+}
+
+std::string_view
+CompletedTransactionLogData::get_pp_tls_cipher() const
+{
+  if (m_http_sm->t_state.pp_info.version != ProxyProtocolVersion::UNDEFINED) {
+    if (auto cipher = m_http_sm->t_state.pp_info.get_tlv_ssl_cipher(); cipher) 
{
+      return *cipher;
+    }
+  }
+  return {};
+}
+
+std::string_view
+CompletedTransactionLogData::get_pp_tls_version() const
+{
+  if (m_http_sm->t_state.pp_info.version != ProxyProtocolVersion::UNDEFINED) {
+    if (auto version = m_http_sm->t_state.pp_info.get_tlv_ssl_version(); 
version) {
+      return *version;
+    }
+  }
+  return {};
+}
+
+// ===== Server response Transfer-Encoding =====
+
+std::string_view
+CompletedTransactionLogData::get_server_response_transfer_encoding() const
+{
+  return m_http_sm->t_state.hdr_info.server_response_transfer_encoding;
+}
diff --git a/src/proxy/http/HttpBodyFactory.cc 
b/src/proxy/http/HttpBodyFactory.cc
index 30a7d2e2d6..05849b781e 100644
--- a/src/proxy/http/HttpBodyFactory.cc
+++ b/src/proxy/http/HttpBodyFactory.cc
@@ -40,6 +40,7 @@
 #include "proxy/hdrs/URL.h"
 #include "proxy/logging/Log.h"
 #include "proxy/logging/LogAccess.h"
+#include "proxy/http/CompletedTransactionLogData.h"
 #include "proxy/hdrs/HttpCompat.h"
 #include "tscore/Layout.h"
 
@@ -1157,7 +1158,8 @@ 
HttpBodyTemplate::build_instantiated_buffer(HttpTransact::State *context, int64_
 
   Dbg(dbg_ctl_body_factory_instantiation, "    before instantiation: [%s]", 
template_buffer);
 
-  LogAccess la(context->state_machine);
+  CompletedTransactionLogData log_data(context->state_machine);
+  LogAccess                   la(log_data);
 
   buffer = resolve_logfield_string(&la, template_buffer);
 
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 2965553918..2bc3654a51 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -45,6 +45,7 @@
 #include "proxy/http/PreWarmConfig.h"
 #include "proxy/logging/Log.h"
 #include "proxy/logging/LogAccess.h"
+#include "proxy/http/CompletedTransactionLogData.h"
 #include "proxy/PluginVC.h"
 #include "proxy/ReverseProxy.h"
 #include "proxy/http/remap/RemapProcessor.h"
@@ -7713,7 +7714,8 @@ HttpSM::kill_this()
     //////////////
     SMDbg(dbg_ctl_http_seq, "Logging transaction");
     if (Log::transaction_logging_enabled() && 
t_state.api_info.logging_enabled) {
-      LogAccess accessor(this);
+      CompletedTransactionLogData log_data(this);
+      LogAccess                   accessor(log_data);
 
       int ret = Log::access(&accessor);
 
@@ -8441,7 +8443,8 @@ HttpSM::do_redirect()
     if (redirect_url != nullptr ||
         
t_state.hdr_info.client_response.field_find(static_cast<std::string_view>(MIME_FIELD_LOCATION)))
 {
       if (Log::transaction_logging_enabled() && 
t_state.api_info.logging_enabled) {
-        LogAccess accessor(this);
+        CompletedTransactionLogData log_data(this);
+        LogAccess                   accessor(log_data);
         if (redirect_url == nullptr) {
           if (t_state.squid_codes.log_code == SquidLogCode::TCP_HIT) {
             t_state.squid_codes.log_code = SquidLogCode::TCP_HIT_REDIRECT;
diff --git a/src/proxy/http2/Http2ConnectionState.cc 
b/src/proxy/http2/Http2ConnectionState.cc
index 4b38579574..d7bf61b860 100644
--- a/src/proxy/http2/Http2ConnectionState.cc
+++ b/src/proxy/http2/Http2ConnectionState.cc
@@ -496,6 +496,9 @@ Http2ConnectionState::rcv_headers_frame(const Http2Frame 
&frame)
         return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, 
Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM,
                           "recv headers enhance your calm");
       } else {
+        if (!stream->trailing_header_is_possible() && 
!stream->is_outbound_connection()) {
+          stream->log_pre_transaction_access(stream->get_receive_header(), 
"http/2");
+        }
         return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM, 
Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR,
                           "recv headers malformed request");
       }
@@ -1108,6 +1111,9 @@ Http2ConnectionState::rcv_continuation_frame(const 
Http2Frame &frame)
         return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, 
Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM,
                           "continuation enhance your calm");
       } else {
+        if (!stream->trailing_header_is_possible() && 
!stream->is_outbound_connection()) {
+          stream->log_pre_transaction_access(stream->get_receive_header(), 
"http/2");
+        }
         return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, 
Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR,
                           "continuation malformed request");
       }
diff --git a/src/proxy/http3/Http3HeaderVIOAdaptor.cc 
b/src/proxy/http3/Http3HeaderVIOAdaptor.cc
index b04347705c..396733d625 100644
--- a/src/proxy/http3/Http3HeaderVIOAdaptor.cc
+++ b/src/proxy/http3/Http3HeaderVIOAdaptor.cc
@@ -22,6 +22,7 @@
  */
 
 #include "proxy/http3/Http3HeaderVIOAdaptor.h"
+#include "proxy/http3/Http3Transaction.h"
 #include "proxy/hdrs/HeaderValidator.h"
 
 #include "iocore/eventsystem/VIO.h"
@@ -34,8 +35,8 @@ DbgCtl dbg_ctl_v_http3{"v_http3"};
 
 } // end anonymous namespace
 
-Http3HeaderVIOAdaptor::Http3HeaderVIOAdaptor(VIO *sink, HTTPType http_type, 
QPACK *qpack, uint64_t stream_id)
-  : _sink_vio(sink), _qpack(qpack), _stream_id(stream_id)
+Http3HeaderVIOAdaptor::Http3HeaderVIOAdaptor(VIO *sink, HTTPType http_type, 
QPACK *qpack, uint64_t stream_id, HQTransaction *txn)
+  : _sink_vio(sink), _qpack(qpack), _stream_id(stream_id), _txn(txn)
 {
   SET_HANDLER(&Http3HeaderVIOAdaptor::event_handler);
 
@@ -108,11 +109,17 @@ Http3HeaderVIOAdaptor::_on_qpack_decode_complete()
   if (!HeaderValidator::is_h2_h3_header_valid(this->_header, 
http_hdr_type_get(this->_header.m_http) == HTTPType::RESPONSE,
                                               NON_TRAILER)) {
     Dbg(dbg_ctl_http3, "Header is invalid");
+    if (this->_txn != nullptr) {
+      this->_txn->log_pre_transaction_access(&this->_header, "http/3");
+    }
     return -1;
   }
   int res = this->_hvc.convert(this->_header, 3, 1);
   if (res != 0) {
     Dbg(dbg_ctl_http3, "ParseResult::ERROR");
+    if (this->_txn != nullptr) {
+      this->_txn->log_pre_transaction_access(&this->_header, "http/3");
+    }
     return -1;
   }
 
diff --git a/src/proxy/http3/Http3Transaction.cc 
b/src/proxy/http3/Http3Transaction.cc
index 3b798c8b13..8c209b774c 100644
--- a/src/proxy/http3/Http3Transaction.cc
+++ b/src/proxy/http3/Http3Transaction.cc
@@ -430,7 +430,7 @@ Http3Transaction::Http3Transaction(Http3Session *session, 
QUICStreamVCAdapter::I
   } else {
     http_type = HTTPType::REQUEST;
   }
-  this->_header_handler = new Http3HeaderVIOAdaptor(&this->_read_vio, 
http_type, session->remote_qpack(), stream_id);
+  this->_header_handler = new Http3HeaderVIOAdaptor(&this->_read_vio, 
http_type, session->remote_qpack(), stream_id, this);
   this->_data_handler   = new Http3StreamDataVIOAdaptor(&this->_read_vio);
 
   this->_frame_dispatcher.add_handler(session->get_received_frame_counter());
diff --git a/src/proxy/logging/CMakeLists.txt b/src/proxy/logging/CMakeLists.txt
index 29139f69d5..f9da235e07 100644
--- a/src/proxy/logging/CMakeLists.txt
+++ b/src/proxy/logging/CMakeLists.txt
@@ -54,6 +54,10 @@ if(BUILD_TESTING)
   target_compile_definitions(test_RolledLogDeleter PRIVATE TEST_LOG_UTILS)
   target_link_libraries(test_RolledLogDeleter tscore ts::inkevent records 
Catch2::Catch2WithMain)
   add_catch2_test(NAME test_RolledLogDeleter COMMAND test_RolledLogDeleter)
+
+  add_executable(test_LogAccess unit-tests/test_LogAccess.cc)
+  target_link_libraries(test_LogAccess ts::logging ts::http ts::inkevent 
records Catch2::Catch2WithMain)
+  add_catch2_test(NAME test_LogAccess COMMAND test_LogAccess)
 endif()
 
 clang_tidy_check(logging)
diff --git a/src/proxy/logging/LogAccess.cc b/src/proxy/logging/LogAccess.cc
index 8921fbf12f..282a04a2fc 100644
--- a/src/proxy/logging/LogAccess.cc
+++ b/src/proxy/logging/LogAccess.cc
@@ -24,21 +24,20 @@
 
 #include "proxy/logging/LogAccess.h"
 
+#include "records/RecCore.h"
 #include "tscore/Version.h"
-#include "proxy/http/HttpSM.h"
 #include "proxy/hdrs/MIME.h"
+#include "proxy/logging/TransactionLogData.h"
 #include "iocore/utils/Machine.h"
 #include "proxy/logging/LogFormat.h"
 #include "proxy/logging/LogBuffer.h"
+#include "records/RecHttp.h"
+#include "swoc/BufferWriter.h"
 #include "tscore/Encoding.h"
-#include "../private/SSLProxySession.h"
 #include "tscore/ink_inet.h"
 
 char INVALID_STR[] = "!INVALID_STR!";
 
-#define HIDDEN_CONTENT_TYPE     "@Content-Type"
-#define HIDDEN_CONTENT_TYPE_LEN 13
-
 // should be at least 22 bytes to always accommodate a converted
 // MgmtInt, MgmtIntCounter or  MgmtFloat. 22 bytes is enough for 64 bit
 // ints + sign + eos, and enough for %e floating point representation
@@ -60,72 +59,63 @@ DbgCtl dbg_ctl_log_unmarshal_data{"log-unmarshal-data"}; // 
Error in txn data wh
 /*-------------------------------------------------------------------------
   LogAccess
 
-  Initialize the private data members and assert that we got a valid state
-  machine pointer.
+  Initialize from TransactionLogData supplied by the caller.
   -------------------------------------------------------------------------*/
 
-LogAccess::LogAccess(HttpSM *sm) : m_http_sm(sm)
-{
-  ink_assert(m_http_sm != nullptr);
-}
-
-/*-------------------------------------------------------------------------
-  LogAccess::init
-  -------------------------------------------------------------------------*/
+LogAccess::LogAccess(TransactionLogData &data) : m_data(&data) {}
 
 void
 LogAccess::init()
 {
-  HttpTransact::HeaderInfo *hdr = &(m_http_sm->t_state.hdr_info);
+  ink_assert(m_data != nullptr);
 
-  if (hdr->client_request.valid()) {
-    m_client_request = &(hdr->client_request);
+  m_client_request = m_data->get_client_request();
 
-    // make a copy of the incoming url into the arena
-    const char *url_string_ref = 
m_client_request->url_string_get_ref(&m_client_req_url_len);
-    m_client_req_url_str       = m_arena.str_alloc(m_client_req_url_len + 1);
-    memcpy(m_client_req_url_str, url_string_ref, m_client_req_url_len);
+  const char *url_str = m_data->get_client_req_url_str();
+  int         url_len = m_data->get_client_req_url_len();
+  if (url_str != nullptr && url_len > 0) {
+    m_client_req_url_len = url_len;
+    m_client_req_url_str = m_arena.str_alloc(m_client_req_url_len + 1);
+    memcpy(m_client_req_url_str, url_str, m_client_req_url_len);
     m_client_req_url_str[m_client_req_url_len] = '\0';
 
     m_client_req_url_canon_str =
       Encoding::escapify_url(&m_arena, m_client_req_url_str, 
m_client_req_url_len, &m_client_req_url_canon_len);
-    auto path{m_client_request->path_get()};
-    m_client_req_url_path_str = path.data();
-    m_client_req_url_path_len = static_cast<int>(path.length());
-  }
-
-  if (hdr->client_response.valid()) {
-    m_proxy_response = &(hdr->client_response);
-    MIMEField *field = 
m_proxy_response->field_find(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE));
-    if (field) {
-      auto proxy_resp_content_type{field->value_get()};
-      m_proxy_resp_content_type_str = const_cast<char 
*>(proxy_resp_content_type.data());
-      m_proxy_resp_content_type_len = proxy_resp_content_type.length();
-      LogUtils::remove_content_type_attributes(m_proxy_resp_content_type_str, 
&m_proxy_resp_content_type_len);
-    } else {
-      // If Content-Type field is missing, check for @Content-Type
-      field = m_proxy_response->field_find(
-        std::string_view{HIDDEN_CONTENT_TYPE, 
static_cast<std::string_view::size_type>(HIDDEN_CONTENT_TYPE_LEN)});
-      if (field) {
-        auto proxy_resp_content_type{field->value_get()};
-        m_proxy_resp_content_type_str = const_cast<char 
*>(proxy_resp_content_type.data());
-        m_proxy_resp_content_type_len = proxy_resp_content_type.length();
-        
LogUtils::remove_content_type_attributes(m_proxy_resp_content_type_str, 
&m_proxy_resp_content_type_len);
-      }
-    }
-    auto reason{m_proxy_response->reason_get()};
-    m_proxy_resp_reason_phrase_str = const_cast<char *>(reason.data());
-    m_proxy_resp_reason_phrase_len = static_cast<int>(reason.length());
   }
-  if (hdr->server_request.valid()) {
-    m_proxy_request = &(hdr->server_request);
+
+  const char *path_str = m_data->get_client_req_url_path_str();
+  int         path_len = m_data->get_client_req_url_path_len();
+  if (path_str != nullptr && path_len > 0) {
+    m_client_req_url_path_len = path_len;
+    char *path_copy           = m_arena.str_alloc(m_client_req_url_path_len + 
1);
+    memcpy(path_copy, path_str, m_client_req_url_path_len);
+    path_copy[m_client_req_url_path_len] = '\0';
+    m_client_req_url_path_str            = path_copy;
+  } else {
+    m_client_req_url_path_str = nullptr;
+    m_client_req_url_path_len = 0;
   }
-  if (hdr->server_response.valid()) {
-    m_server_response = &(hdr->server_response);
+
+  m_proxy_response = m_data->get_proxy_response();
+
+  char *ct_str = m_data->get_proxy_resp_content_type_str();
+  int   ct_len = m_data->get_proxy_resp_content_type_len();
+  if (ct_str != nullptr && ct_len > 0) {
+    m_proxy_resp_content_type_str = ct_str;
+    m_proxy_resp_content_type_len = ct_len;
+    LogUtils::remove_content_type_attributes(m_proxy_resp_content_type_str, 
&m_proxy_resp_content_type_len);
   }
-  if (hdr->cache_response.valid()) {
-    m_cache_response = &(hdr->cache_response);
+
+  char *reason_str = m_data->get_proxy_resp_reason_phrase_str();
+  int   reason_len = m_data->get_proxy_resp_reason_phrase_len();
+  if (reason_str != nullptr && reason_len > 0) {
+    m_proxy_resp_reason_phrase_str = reason_str;
+    m_proxy_resp_reason_phrase_len = reason_len;
   }
+
+  m_proxy_request   = m_data->get_proxy_request();
+  m_server_response = m_data->get_server_response();
+  m_cache_response  = m_data->get_cache_response();
 }
 
 int
@@ -512,8 +502,7 @@ LogAccess::has_http_header_field(LogField::Container 
container, const char *fiel
   }
 
   if (container == LogField::SSH && strcmp(field, "Transfer-Encoding") == 0) {
-    const std::string &stored_te = 
m_http_sm->t_state.hdr_info.server_response_transfer_encoding;
-    if (!stored_te.empty()) {
+    if (!m_data->get_server_response_transfer_encoding().empty()) {
       return true;
     }
   }
@@ -1452,9 +1441,7 @@ LogAccess::set_client_req_url_path(char *buf, int len)
 /*-------------------------------------------------------------------------
   The marshalling routines ...
 
-  We know that m_http_sm is a valid pointer (we assert so in the ctor), but
-  we still need to check the other header pointers before using them in the
-  routines.
+  Header pointers may be null; each routine must guard before use.
   -------------------------------------------------------------------------*/
 
 /*-------------------------------------------------------------------------
@@ -1463,7 +1450,7 @@ int
 LogAccess::marshal_plugin_identity_id(char *buf)
 {
   if (buf) {
-    marshal_int(buf, m_http_sm->plugin_id);
+    marshal_int(buf, m_data->get_plugin_id());
   }
   return INK_MIN_ALIGN;
 }
@@ -1472,7 +1459,7 @@ int
 LogAccess::marshal_plugin_identity_tag(char *buf)
 {
   int         len = INK_MIN_ALIGN;
-  const char *tag = m_http_sm->plugin_tag;
+  const char *tag = m_data->get_plugin_tag();
 
   if (!tag) {
     tag = "*";
@@ -1490,33 +1477,28 @@ LogAccess::marshal_plugin_identity_tag(char *buf)
 int
 LogAccess::marshal_client_host_ip(char *buf)
 {
-  return marshal_ip(buf, &m_http_sm->t_state.effective_client_addr.sa);
+  return marshal_ip(buf, m_data->get_client_addr());
 }
 
 int
 LogAccess::marshal_remote_host_ip(char *buf)
 {
-  return marshal_ip(buf, &m_http_sm->t_state.client_info.src_addr.sa);
+  return marshal_ip(buf, m_data->get_client_src_addr());
 }
 
 int
 LogAccess::marshal_host_interface_ip(char *buf)
 {
-  return marshal_ip(buf, &m_http_sm->t_state.client_info.dst_addr.sa);
+  return marshal_ip(buf, m_data->get_client_dst_addr());
 }
 
 int
 LogAccess::marshal_client_host_ip_verified(char *buf)
 {
-  if (m_http_sm) {
-    auto txn = m_http_sm->get_ua_txn();
-    if (txn) {
-      sockaddr const *addr = txn->get_verified_client_addr();
-      if (addr && ats_is_ip(addr)) {
-        return marshal_ip(buf, addr);
-      }
-    }
+  if (m_data->get_verified_client_addr()) {
+    return marshal_ip(buf, m_data->get_verified_client_addr());
   }
+
   return marshal_client_host_ip(buf);
 }
 
@@ -1546,29 +1528,11 @@ LogAccess::marshal_cache_lookup_url_canon(char *buf)
 int
 LogAccess::marshal_client_sni_server_name(char *buf)
 {
-  // NOTE:  For this string_view, data() must always be nul-terminated, but 
the nul character must not be included in
-  // the length.
-  //
-  std::string_view server_name = "";
-
-  if (m_http_sm) {
-    auto txn = m_http_sm->get_ua_txn();
-    if (txn) {
-      auto ssn = txn->get_proxy_ssn();
-      if (ssn) {
-        auto ssl = ssn->ssl();
-        if (ssl) {
-          auto server_name_str = ssl->client_sni_server_name();
-          if (server_name_str) {
-            server_name = server_name_str;
-          }
-        }
-      }
-    }
-  }
-  int len = padded_length(server_name.length() + 1);
+  const char *sni              = m_data->get_sni_server_name();
+  const char *server_name_cstr = sni ? sni : "";
+  int         len              = LogAccess::padded_strlen(server_name_cstr);
   if (buf) {
-    marshal_str(buf, server_name.data(), len);
+    marshal_str(buf, server_name_cstr, len);
   }
   return len;
 }
@@ -1578,21 +1542,8 @@ LogAccess::marshal_client_sni_server_name(char *buf)
 int
 LogAccess::marshal_client_provided_cert(char *buf)
 {
-  int provided_cert = 0;
-  if (m_http_sm) {
-    auto txn = m_http_sm->get_ua_txn();
-    if (txn) {
-      auto ssn = txn->get_proxy_ssn();
-      if (ssn) {
-        auto ssl = ssn->ssl();
-        if (ssl) {
-          provided_cert = ssl->client_provided_certificate();
-        }
-      }
-    }
-  }
   if (buf) {
-    marshal_int(buf, provided_cert);
+    marshal_int(buf, m_data->get_client_provided_cert());
   }
   return INK_MIN_ALIGN;
 }
@@ -1602,12 +1553,8 @@ LogAccess::marshal_client_provided_cert(char *buf)
 int
 LogAccess::marshal_proxy_provided_cert(char *buf)
 {
-  int provided_cert = 0;
-  if (m_http_sm) {
-    provided_cert = m_http_sm->server_connection_provided_cert;
-  }
   if (buf) {
-    marshal_int(buf, provided_cert);
+    marshal_int(buf, m_data->get_server_connection_provided_cert());
   }
   return INK_MIN_ALIGN;
 }
@@ -1645,26 +1592,19 @@ LogAccess::marshal_version_string(char *buf)
 int
 LogAccess::marshal_proxy_protocol_version(char *buf)
 {
-  const char *version_str = nullptr;
-  int         len         = INK_MIN_ALIGN;
-
-  if (m_http_sm) {
-    ProxyProtocolVersion ver = m_http_sm->t_state.pp_info.version;
-    switch (ver) {
-    case ProxyProtocolVersion::V1:
-      version_str = "V1";
-      break;
-    case ProxyProtocolVersion::V2:
-      version_str = "V2";
-      break;
-    case ProxyProtocolVersion::UNDEFINED:
-    default:
-      version_str = "-";
-      break;
-    }
-    len = LogAccess::padded_strlen(version_str);
+  const char *version_str = "-";
+  switch (m_data->get_pp_version()) {
+  case 1:
+    version_str = "V1";
+    break;
+  case 2:
+    version_str = "V2";
+    break;
+  default:
+    version_str = "-";
+    break;
   }
-
+  int len = LogAccess::padded_strlen(version_str);
   if (buf) {
     marshal_str(buf, version_str, len);
   }
@@ -1677,8 +1617,8 @@ int
 LogAccess::marshal_proxy_protocol_src_ip(char *buf)
 {
   sockaddr const *ip = nullptr;
-  if (m_http_sm && m_http_sm->t_state.pp_info.version != 
ProxyProtocolVersion::UNDEFINED) {
-    ip = &m_http_sm->t_state.pp_info.src_addr.sa;
+  if (m_data->get_pp_version() != 0) {
+    ip = m_data->get_pp_src_addr();
   }
   return marshal_ip(buf, ip);
 }
@@ -1689,8 +1629,8 @@ int
 LogAccess::marshal_proxy_protocol_dst_ip(char *buf)
 {
   sockaddr const *ip = nullptr;
-  if (m_http_sm && m_http_sm->t_state.pp_info.version != 
ProxyProtocolVersion::UNDEFINED) {
-    ip = &m_http_sm->t_state.pp_info.dst_addr.sa;
+  if (m_data->get_pp_version() != 0) {
+    ip = m_data->get_pp_dst_addr();
   }
   return marshal_ip(buf, ip);
 }
@@ -1698,15 +1638,13 @@ LogAccess::marshal_proxy_protocol_dst_ip(char *buf)
 int
 LogAccess::marshal_proxy_protocol_authority(char *buf)
 {
-  int len = INK_MIN_ALIGN;
+  int              len       = INK_MIN_ALIGN;
+  std::string_view authority = m_data->get_pp_authority();
 
-  if (m_http_sm) {
-    if (auto authority = 
m_http_sm->t_state.pp_info.get_tlv(PP2_TYPE_AUTHORITY)) {
-      len = padded_length(authority->size() + 1);
-      if (buf) {
-        marshal_str(buf, authority->data(), len);
-        buf[authority->size()] = '\0';
-      }
+  if (!authority.empty()) {
+    len = padded_length(static_cast<int>(authority.size()) + 1);
+    if (buf) {
+      marshal_mem(buf, authority.data(), static_cast<int>(authority.size()), 
len);
     }
   }
   return len;
@@ -1715,19 +1653,17 @@ LogAccess::marshal_proxy_protocol_authority(char *buf)
 int
 LogAccess::marshal_proxy_protocol_tls_cipher(char *buf)
 {
-  int len = INK_MIN_ALIGN;
+  int              len    = INK_MIN_ALIGN;
+  std::string_view cipher = m_data->get_pp_tls_cipher();
 
-  if (m_http_sm) {
-    if (auto cipher = m_http_sm->t_state.pp_info.get_tlv_ssl_cipher(); cipher) 
{
-      len = padded_length(cipher->size() + 1);
-      if (buf) {
-        marshal_mem(buf, cipher->data(), cipher->size(), len);
-      }
-    } else {
-      if (buf) {
-        // This prints the default value ("-")
-        marshal_mem(buf, nullptr, 0, len);
-      }
+  if (!cipher.empty()) {
+    len = padded_length(static_cast<int>(cipher.size()) + 1);
+    if (buf) {
+      marshal_mem(buf, cipher.data(), static_cast<int>(cipher.size()), len);
+    }
+  } else {
+    if (buf) {
+      marshal_mem(buf, nullptr, 0, len);
     }
   }
   return len;
@@ -1736,19 +1672,17 @@ LogAccess::marshal_proxy_protocol_tls_cipher(char *buf)
 int
 LogAccess::marshal_proxy_protocol_tls_version(char *buf)
 {
-  int len = INK_MIN_ALIGN;
+  int              len     = INK_MIN_ALIGN;
+  std::string_view version = m_data->get_pp_tls_version();
 
-  if (m_http_sm) {
-    if (auto version = m_http_sm->t_state.pp_info.get_tlv_ssl_version(); 
version) {
-      len = padded_length(version->size() + 1);
-      if (buf) {
-        marshal_mem(buf, version->data(), version->size(), len);
-      }
-    } else {
-      if (buf) {
-        // This prints the default value ("-")
-        marshal_mem(buf, nullptr, 0, len);
-      }
+  if (!version.empty()) {
+    len = padded_length(static_cast<int>(version.size()) + 1);
+    if (buf) {
+      marshal_mem(buf, version.data(), static_cast<int>(version.size()), len);
+    }
+  } else {
+    if (buf) {
+      marshal_mem(buf, nullptr, 0, len);
     }
   }
   return len;
@@ -1759,12 +1693,8 @@ LogAccess::marshal_proxy_protocol_tls_version(char *buf)
 int
 LogAccess::marshal_client_host_port(char *buf)
 {
-  if (m_http_sm) {
-    auto txn = m_http_sm->get_ua_txn();
-    if (txn) {
-      uint16_t port = txn->get_client_port();
-      marshal_int(buf, port);
-    }
+  if (buf) {
+    marshal_int(buf, m_data->get_client_port());
   }
   return INK_MIN_ALIGN;
 }
@@ -1773,7 +1703,8 @@ int
 LogAccess::marshal_remote_host_port(char *buf)
 {
   if (buf) {
-    uint16_t port = m_http_sm->t_state.client_info.src_addr.host_order_port();
+    sockaddr const *src  = m_data->get_client_src_addr();
+    uint16_t        port = src ? ats_ip_port_host_order(src) : 0;
     marshal_int(buf, port);
   }
   return INK_MIN_ALIGN;
@@ -1792,8 +1723,8 @@ LogAccess::marshal_client_auth_user_name(char *buf)
   // Jira TS-40:
   // NOTE: Authentication related code and modules were removed/disabled.
   //       Uncomment code path below when re-added/enabled.
-  /*if (m_http_sm->t_state.auth_params.user_name) {
-    str = m_http_sm->t_state.auth_params.user_name;
+  /*if (auth_params.user_name) {
+    str = auth_params.user_name;
     len = LogAccess::strlen(str);
     } */
   if (buf) {
@@ -1810,17 +1741,22 @@ LogAccess::marshal_client_auth_user_name(char *buf)
 void
 LogAccess::validate_unmapped_url()
 {
+  char *unmapped_url_str = m_data->get_unmapped_url_str();
+  int   unmapped_url_len = m_data->get_unmapped_url_len();
+
+  if (unmapped_url_str == nullptr) {
+    m_client_req_unmapped_url_canon_str = INVALID_STR;
+    return;
+  }
+
   if (m_client_req_unmapped_url_canon_str == nullptr) {
     // prevent multiple validations
     m_client_req_unmapped_url_canon_str = INVALID_STR;
 
-    if (m_http_sm->t_state.unmapped_url.valid()) {
-      int   unmapped_url_len;
-      char *unmapped_url = 
m_http_sm->t_state.unmapped_url.string_get_ref(&unmapped_url_len);
-
-      if (unmapped_url && unmapped_url[0] != 0) {
+    if (unmapped_url_len > 0) {
+      if (unmapped_url_str && unmapped_url_str[0] != 0) {
         m_client_req_unmapped_url_canon_str =
-          Encoding::escapify_url(&m_arena, unmapped_url, unmapped_url_len, 
&m_client_req_unmapped_url_canon_len);
+          Encoding::escapify_url(&m_arena, unmapped_url_str, unmapped_url_len, 
&m_client_req_unmapped_url_canon_len);
       }
     }
   }
@@ -1872,16 +1808,22 @@ LogAccess::validate_unmapped_url_path()
 void
 LogAccess::validate_lookup_url()
 {
+  char *lookup_url_str = m_data->get_cache_lookup_url_str();
+  int   lookup_url_len = m_data->get_cache_lookup_url_len();
+
+  if (lookup_url_str == nullptr) {
+    m_cache_lookup_url_canon_str = INVALID_STR;
+    return;
+  }
+
   if (m_cache_lookup_url_canon_str == nullptr) {
     // prevent multiple validations
     m_cache_lookup_url_canon_str = INVALID_STR;
 
-    if (m_http_sm->t_state.cache_info.lookup_url_storage.valid()) {
-      int   lookup_url_len;
-      char *lookup_url = 
m_http_sm->t_state.cache_info.lookup_url_storage.string_get_ref(&lookup_url_len);
-
-      if (lookup_url && lookup_url[0] != 0) {
-        m_cache_lookup_url_canon_str = Encoding::escapify_url(&m_arena, 
lookup_url, lookup_url_len, &m_cache_lookup_url_canon_len);
+    if (lookup_url_len > 0) {
+      if (lookup_url_str && lookup_url_str[0] != 0) {
+        m_cache_lookup_url_canon_str =
+          Encoding::escapify_url(&m_arena, lookup_url_str, lookup_url_len, 
&m_cache_lookup_url_canon_len);
       }
     }
   }
@@ -1936,15 +1878,19 @@ LogAccess::marshal_client_req_http_method(char *buf)
 
   if (m_client_request) {
     str = m_client_request->method_get();
+  }
 
-    // calculate the padded length only if the actual length
-    // is not zero. We don't want the padded length to be zero
-    // because marshal_mem should write the DEFAULT_STR to the
-    // buffer if str is nil, and we need room for this.
-    //
-    if (!str.empty()) {
-      plen = padded_length(static_cast<int>(str.length()) + 1); // +1 for 
trailing 0
-    }
+  if (str.empty()) {
+    str = m_data->get_method();
+  }
+
+  // calculate the padded length only if the actual length
+  // is not zero. We don't want the padded length to be zero
+  // because marshal_mem should write the DEFAULT_STR to the
+  // buffer if str is nil, and we need room for this.
+  //
+  if (!str.empty()) {
+    plen = padded_length(static_cast<int>(str.length()) + 1); // +1 for 
trailing 0
   }
 
   if (buf) {
@@ -2057,20 +2003,23 @@ LogAccess::marshal_client_req_url_path(char *buf)
 int
 LogAccess::marshal_client_req_url_scheme(char *buf)
 {
-  int         scheme = m_http_sm->t_state.orig_scheme;
-  const char *str    = nullptr;
-  int         alen;
-  int         plen = INK_MIN_ALIGN;
+  const char      *str       = nullptr;
+  int              alen      = 0;
+  int              plen      = INK_MIN_ALIGN;
+  int              scheme    = m_data->get_orig_scheme();
+  std::string_view scheme_sv = m_data->get_scheme();
 
-  // If the transaction aborts very early, the scheme may not be set, or so 
ASAN reports.
   if (scheme >= 0) {
     str  = hdrtoken_index_to_wks(scheme);
     alen = hdrtoken_index_to_length(scheme);
+  } else if (!scheme_sv.empty()) {
+    str  = scheme_sv.data();
+    alen = static_cast<int>(scheme_sv.size());
   } else {
     str  = "UNKNOWN";
-    alen = ::strlen(str);
+    alen = 7;
   }
-  plen = padded_length(alen + 1); // +1 for trailing 0
+  plen = padded_length(alen + 1);
 
   if (buf) {
     marshal_mem(buf, str, alen, plen);
@@ -2106,8 +2055,17 @@ LogAccess::marshal_client_req_http_version(char *buf)
 int
 LogAccess::marshal_client_req_protocol_version(char *buf)
 {
-  const char *protocol_str = m_http_sm->get_user_agent().get_client_protocol();
-  int         len          = LogAccess::padded_strlen(protocol_str);
+  const char      *protocol_str = m_data->get_client_protocol();
+  std::string_view protocol_sv  = m_data->get_client_protocol_str();
+  if (protocol_str == nullptr || protocol_str[0] == '\0') {
+    if (!protocol_sv.empty()) {
+      protocol_str = protocol_sv.data();
+    } else {
+      protocol_str = DEFAULT_STR;
+    }
+  }
+
+  int len = LogAccess::padded_strlen(protocol_str);
 
   // Set major & minor versions when protocol_str is not "http/2".
   if (::strlen(protocol_str) == 4 && strncmp("http", protocol_str, 4) == 0) {
@@ -2138,7 +2096,8 @@ LogAccess::marshal_client_req_protocol_version(char *buf)
 int
 LogAccess::marshal_server_req_protocol_version(char *buf)
 {
-  const char *protocol_str = m_http_sm->server_protocol;
+  const char *server_proto = m_data->get_server_protocol();
+  const char *protocol_str = server_proto ? server_proto : DEFAULT_STR;
   int         len          = LogAccess::padded_strlen(protocol_str);
 
   // Set major & minor versions when protocol_str is not "http/2".
@@ -2189,7 +2148,7 @@ LogAccess::marshal_client_req_content_len(char *buf)
   if (buf) {
     int64_t len = 0;
     if (m_client_request) {
-      len = m_http_sm->client_request_body_bytes;
+      len = m_data->get_client_request_body_bytes();
     }
     marshal_int(buf, len);
   }
@@ -2202,7 +2161,7 @@ LogAccess::marshal_client_req_squid_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_client_request) {
-      val = m_client_request->length_get() + 
m_http_sm->client_request_body_bytes;
+      val = m_client_request->length_get() + 
m_data->get_client_request_body_bytes();
     }
     marshal_int(buf, val);
   }
@@ -2216,7 +2175,8 @@ int
 LogAccess::marshal_client_req_tcp_reused(char *buf)
 {
   if (buf) {
-    marshal_int(buf, m_http_sm->get_user_agent().get_client_tcp_reused() ? 1 : 
0);
+    int64_t val = m_data->get_client_tcp_reused() ? 1 : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -2225,7 +2185,8 @@ int
 LogAccess::marshal_client_req_is_ssl(char *buf)
 {
   if (buf) {
-    marshal_int(buf, 
m_http_sm->get_user_agent().get_client_connection_is_ssl() ? 1 : 0);
+    int64_t val = m_data->get_client_connection_is_ssl() ? 1 : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -2234,7 +2195,8 @@ int
 LogAccess::marshal_client_req_ssl_reused(char *buf)
 {
   if (buf) {
-    marshal_int(buf, m_http_sm->get_user_agent().get_client_ssl_reused() ? 1 : 
0);
+    int64_t val = m_data->get_client_ssl_reused() ? 1 : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -2243,7 +2205,7 @@ int
 LogAccess::marshal_client_ssl_resumption_type(char *buf)
 {
   if (buf) {
-    marshal_int(buf, 
m_http_sm->get_user_agent().get_client_ssl_resumption_type());
+    marshal_int(buf, m_data->get_client_ssl_resumption_type());
   }
   return INK_MIN_ALIGN;
 }
@@ -2252,7 +2214,8 @@ int
 LogAccess::marshal_client_req_is_internal(char *buf)
 {
   if (buf) {
-    marshal_int(buf, m_http_sm->is_internal ? 1 : 0);
+    int64_t val = m_data->get_is_internal() ? 1 : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -2261,11 +2224,11 @@ int
 LogAccess::marshal_client_req_mptcp_state(char *buf)
 {
   if (buf) {
-    int val = -1;
+    int                 val         = -1;
+    std::optional<bool> mptcp_state = m_data->get_mptcp_state();
 
-    if (m_http_sm->mptcp_state.has_value()) {
-      val = m_http_sm->mptcp_state.value() ? 1 : 0;
-    } else {
+    if (mptcp_state.has_value()) {
+      val = mptcp_state.value() ? 1 : 0;
     }
     marshal_int(buf, val);
   }
@@ -2279,18 +2242,7 @@ int
 LogAccess::marshal_client_finish_status_code(char *buf)
 {
   if (buf) {
-    int                        code           = LOG_FINISH_FIN;
-    HttpTransact::AbortState_t cl_abort_state = 
m_http_sm->t_state.client_info.abort;
-    if (cl_abort_state == HttpTransact::ABORTED) {
-      // Check to see if the abort is due to a timeout
-      if (m_http_sm->t_state.client_info.state == HttpTransact::ACTIVE_TIMEOUT 
||
-          m_http_sm->t_state.client_info.state == 
HttpTransact::INACTIVE_TIMEOUT) {
-        code = LOG_FINISH_TIMEOUT;
-      } else {
-        code = LOG_FINISH_INTR;
-      }
-    }
-    marshal_int(buf, code);
+    marshal_int(buf, m_data->get_client_finish_status_code());
   }
   return INK_MIN_ALIGN;
 }
@@ -2302,7 +2254,7 @@ int
 LogAccess::marshal_client_req_id(char *buf)
 {
   if (buf) {
-    marshal_int(buf, m_http_sm->sm_id);
+    marshal_int(buf, m_data->get_sm_id());
   }
   return INK_MIN_ALIGN;
 }
@@ -2314,8 +2266,9 @@ int
 LogAccess::marshal_client_req_uuid(char *buf)
 {
   char        str[TS_CRUUID_STRING_LEN + 1];
-  const char *uuid = Machine::instance()->process_uuid.getString();
-  int         len  = snprintf(str, sizeof(str), "%s-%" PRId64 "", uuid, 
m_http_sm->sm_id);
+  const char *uuid   = Machine::instance()->process_uuid.getString();
+  int64_t     req_id = m_data->get_sm_id();
+  int         len    = snprintf(str, sizeof(str), "%s-%" PRId64 "", uuid, 
req_id);
 
   ink_assert(len <= TS_CRUUID_STRING_LEN);
   len = padded_length(len + 1);
@@ -2330,18 +2283,14 @@ LogAccess::marshal_client_req_uuid(char *buf)
 /*-------------------------------------------------------------------------
   -------------------------------------------------------------------------*/
 
-// 1 ('S'/'T' flag) + 8 (Error Code) + 1 ('\0')
-static constexpr size_t MAX_PROXY_ERROR_CODE_SIZE = 10;
-
 int
 LogAccess::marshal_client_rx_error_code(char *buf)
 {
-  char error_code[MAX_PROXY_ERROR_CODE_SIZE] = {0};
-  m_http_sm->t_state.client_info.rx_error_code.str(error_code, 
sizeof(error_code));
-  int round_len = LogAccess::padded_strlen(error_code);
+  const char *err_code  = m_data->get_client_rx_error_code();
+  int         round_len = LogAccess::padded_strlen(err_code);
 
   if (buf) {
-    marshal_str(buf, error_code, round_len);
+    marshal_str(buf, err_code, round_len);
   }
 
   return round_len;
@@ -2350,12 +2299,11 @@ LogAccess::marshal_client_rx_error_code(char *buf)
 int
 LogAccess::marshal_client_tx_error_code(char *buf)
 {
-  char error_code[MAX_PROXY_ERROR_CODE_SIZE] = {0};
-  m_http_sm->t_state.client_info.tx_error_code.str(error_code, 
sizeof(error_code));
-  int round_len = LogAccess::padded_strlen(error_code);
+  const char *err_code  = m_data->get_client_tx_error_code();
+  int         round_len = LogAccess::padded_strlen(err_code);
 
   if (buf) {
-    marshal_str(buf, error_code, round_len);
+    marshal_str(buf, err_code, round_len);
   }
 
   return round_len;
@@ -2366,7 +2314,8 @@ LogAccess::marshal_client_tx_error_code(char *buf)
 int
 LogAccess::marshal_client_security_protocol(char *buf)
 {
-  const char *proto     = 
m_http_sm->get_user_agent().get_client_sec_protocol();
+  const char *sec_proto = m_data->get_client_sec_protocol();
+  const char *proto     = sec_proto ? sec_proto : DEFAULT_STR;
   int         round_len = LogAccess::padded_strlen(proto);
 
   if (buf) {
@@ -2379,8 +2328,9 @@ LogAccess::marshal_client_security_protocol(char *buf)
 int
 LogAccess::marshal_client_security_cipher_suite(char *buf)
 {
-  const char *cipher    = 
m_http_sm->get_user_agent().get_client_cipher_suite();
-  int         round_len = LogAccess::padded_strlen(cipher);
+  const char *cipher_suite = m_data->get_client_cipher_suite();
+  const char *cipher       = cipher_suite ? cipher_suite : DEFAULT_STR;
+  int         round_len    = LogAccess::padded_strlen(cipher);
 
   if (buf) {
     marshal_str(buf, cipher, round_len);
@@ -2392,8 +2342,9 @@ LogAccess::marshal_client_security_cipher_suite(char *buf)
 int
 LogAccess::marshal_client_security_curve(char *buf)
 {
-  const char *curve     = m_http_sm->get_user_agent().get_client_curve();
-  int         round_len = LogAccess::padded_strlen(curve);
+  const char *client_curve = m_data->get_client_curve();
+  const char *curve        = client_curve ? client_curve : DEFAULT_STR;
+  int         round_len    = LogAccess::padded_strlen(curve);
 
   if (buf) {
     marshal_str(buf, curve, round_len);
@@ -2405,7 +2356,8 @@ LogAccess::marshal_client_security_curve(char *buf)
 int
 LogAccess::marshal_client_security_group(char *buf)
 {
-  const char *group     = 
m_http_sm->get_user_agent().get_client_security_group();
+  const char *sec_group = m_data->get_client_security_group();
+  const char *group     = sec_group ? sec_group : DEFAULT_STR;
   int         round_len = LogAccess::padded_strlen(group);
 
   if (buf) {
@@ -2418,8 +2370,9 @@ LogAccess::marshal_client_security_group(char *buf)
 int
 LogAccess::marshal_client_security_alpn(char *buf)
 {
-  const char *alpn = "-";
-  if (const int alpn_id = m_http_sm->get_user_agent().get_client_alpn_id(); 
alpn_id != SessionProtocolNameRegistry::INVALID) {
+  const char *alpn    = "-";
+  int         alpn_id = m_data->get_client_alpn_id();
+  if (alpn_id != SessionProtocolNameRegistry::INVALID) {
     swoc::TextView client_sec_alpn = 
globalSessionProtocolNameRegistry.nameFor(alpn_id);
     alpn                           = client_sec_alpn.data();
   }
@@ -2467,7 +2420,7 @@ int
 LogAccess::marshal_proxy_resp_squid_len(char *buf)
 {
   if (buf) {
-    int64_t val = m_http_sm->client_response_hdr_bytes + 
m_http_sm->client_response_body_bytes;
+    int64_t val = m_data->get_client_response_hdr_bytes() + 
m_data->get_client_response_body_bytes();
     marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
@@ -2480,8 +2433,7 @@ int
 LogAccess::marshal_proxy_resp_content_len(char *buf)
 {
   if (buf) {
-    int64_t val = m_http_sm->client_response_body_bytes;
-    marshal_int(buf, val);
+    marshal_int(buf, m_data->get_client_response_body_bytes());
   }
   return INK_MIN_ALIGN;
 }
@@ -2520,15 +2472,13 @@ LogAccess::marshal_proxy_resp_status_code(char *buf)
 int
 LogAccess::marshal_status_plugin_entry(char *buf)
 {
-  char const *str = nullptr;
-  int         len = INK_MIN_ALIGN;
+  char const      *str         = nullptr;
+  int              len         = INK_MIN_ALIGN;
+  std::string_view setter_name = m_data->get_http_return_code_setter_name();
 
-  if (m_http_sm) {
-    std::string const &tag = m_http_sm->t_state.http_return_code_setter_name;
-    if (!tag.empty()) {
-      str = tag.c_str();
-      len = LogAccess::padded_strlen(str);
-    }
+  if (!setter_name.empty()) {
+    str = setter_name.data();
+    len = LogAccess::padded_strlen(str);
   }
 
   if (buf) {
@@ -2544,8 +2494,7 @@ int
 LogAccess::marshal_proxy_resp_header_len(char *buf)
 {
   if (buf) {
-    int64_t val = m_http_sm->client_response_hdr_bytes;
-    marshal_int(buf, val);
+    marshal_int(buf, m_data->get_client_response_hdr_bytes());
   }
   return INK_MIN_ALIGN;
 }
@@ -2553,29 +2502,8 @@ LogAccess::marshal_proxy_resp_header_len(char *buf)
 int
 LogAccess::marshal_proxy_finish_status_code(char *buf)
 {
-  /* FIXME: Should there be no server transaction code if
-     the result comes out of the cache.  Right now we default
-     to FIN */
-  if (buf) {
-    int code = LOG_FINISH_FIN;
-    if (m_http_sm->t_state.current.server) {
-      switch (m_http_sm->t_state.current.server->state) {
-      case HttpTransact::ACTIVE_TIMEOUT:
-      case HttpTransact::INACTIVE_TIMEOUT:
-        code = LOG_FINISH_TIMEOUT;
-        break;
-      case HttpTransact::CONNECTION_ERROR:
-        code = LOG_FINISH_INTR;
-        break;
-      default:
-        if (m_http_sm->t_state.current.server->abort == HttpTransact::ABORTED) 
{
-          code = LOG_FINISH_INTR;
-        }
-        break;
-      }
-    }
-
-    marshal_int(buf, code);
+  if (buf) {
+    marshal_int(buf, m_data->get_proxy_finish_status_code());
   }
 
   return INK_MIN_ALIGN;
@@ -2587,8 +2515,7 @@ int
 LogAccess::marshal_proxy_host_port(char *buf)
 {
   if (buf) {
-    uint16_t port = m_http_sm->t_state.request_data.incoming_port;
-    marshal_int(buf, port);
+    marshal_int(buf, static_cast<int64_t>(m_data->get_incoming_port()));
   }
   return INK_MIN_ALIGN;
 }
@@ -2600,8 +2527,7 @@ int
 LogAccess::marshal_cache_result_code(char *buf)
 {
   if (buf) {
-    SquidLogCode code = m_http_sm->t_state.squid_codes.log_code;
-    marshal_int(buf, static_cast<int64_t>(code));
+    marshal_int(buf, static_cast<int64_t>(m_data->get_log_code()));
   }
   return INK_MIN_ALIGN;
 }
@@ -2613,8 +2539,7 @@ int
 LogAccess::marshal_cache_result_subcode(char *buf)
 {
   if (buf) {
-    SquidSubcode code = m_http_sm->t_state.squid_codes.subcode;
-    marshal_int(buf, static_cast<int64_t>(code));
+    marshal_int(buf, static_cast<int64_t>(m_data->get_subcode()));
   }
   return INK_MIN_ALIGN;
 }
@@ -2626,8 +2551,7 @@ int
 LogAccess::marshal_cache_hit_miss(char *buf)
 {
   if (buf) {
-    SquidHitMissCode code = m_http_sm->t_state.squid_codes.hit_miss_code;
-    marshal_int(buf, static_cast<int64_t>(code));
+    marshal_int(buf, static_cast<int64_t>(m_data->get_hit_miss_code()));
   }
   return INK_MIN_ALIGN;
 }
@@ -2657,7 +2581,7 @@ LogAccess::marshal_proxy_req_content_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_proxy_request) {
-      val = m_http_sm->server_request_body_bytes;
+      val = m_data->get_server_request_body_bytes();
     }
     marshal_int(buf, val);
   }
@@ -2670,7 +2594,7 @@ LogAccess::marshal_proxy_req_squid_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_proxy_request) {
-      val = m_proxy_request->length_get() + 
m_http_sm->server_request_body_bytes;
+      val = m_proxy_request->length_get() + 
m_data->get_server_request_body_bytes();
     }
     marshal_int(buf, val);
   }
@@ -2681,15 +2605,18 @@ LogAccess::marshal_proxy_req_squid_len(char *buf)
 int
 LogAccess::marshal_proxy_req_server_ip(char *buf)
 {
-  return marshal_ip(buf, m_http_sm->t_state.current.server != nullptr ? 
&m_http_sm->t_state.current.server->src_addr.sa : nullptr);
+  return marshal_ip(buf, m_data->get_server_src_addr());
 }
 
 int
 LogAccess::marshal_proxy_req_server_port(char *buf)
 {
   if (buf) {
-    uint16_t port =
-      m_http_sm->t_state.current.server != nullptr ? 
m_http_sm->t_state.current.server->src_addr.host_order_port() : 0;
+    uint16_t        port = 0;
+    sockaddr const *src  = m_data->get_server_src_addr();
+    if (src) {
+      port = ats_ip_port_host_order(src);
+    }
     marshal_int(buf, port);
   }
   return INK_MIN_ALIGN;
@@ -2698,15 +2625,18 @@ LogAccess::marshal_proxy_req_server_port(char *buf)
 int
 LogAccess::marshal_next_hop_ip(char *buf)
 {
-  return marshal_ip(buf, m_http_sm->t_state.current.server != nullptr ? 
&m_http_sm->t_state.current.server->dst_addr.sa : nullptr);
+  return marshal_ip(buf, m_data->get_server_dst_addr());
 }
 
 int
 LogAccess::marshal_next_hop_port(char *buf)
 {
   if (buf) {
-    uint16_t port =
-      m_http_sm->t_state.current.server != nullptr ? 
m_http_sm->t_state.current.server->dst_addr.host_order_port() : 0;
+    uint16_t        port = 0;
+    sockaddr const *dst  = m_data->get_server_dst_addr();
+    if (dst) {
+      port = ats_ip_port_host_order(dst);
+    }
     marshal_int(buf, port);
   }
   return INK_MIN_ALIGN;
@@ -2719,8 +2649,7 @@ int
 LogAccess::marshal_proxy_req_is_ssl(char *buf)
 {
   if (buf) {
-    int64_t is_ssl;
-    is_ssl = m_http_sm->server_connection_is_ssl;
+    int64_t is_ssl = m_data->get_server_connection_is_ssl() ? 1 : 0;
     marshal_int(buf, is_ssl);
   }
   return INK_MIN_ALIGN;
@@ -2730,7 +2659,7 @@ int
 LogAccess::marshal_proxy_req_ssl_reused(char *buf)
 {
   if (buf) {
-    marshal_int(buf, m_http_sm->server_ssl_reused ? 1 : 0);
+    marshal_int(buf, m_data->get_server_ssl_reused() ? 1 : 0);
   }
   return INK_MIN_ALIGN;
 }
@@ -2742,8 +2671,7 @@ int
 LogAccess::marshal_proxy_hierarchy_route(char *buf)
 {
   if (buf) {
-    SquidHierarchyCode code = m_http_sm->t_state.squid_codes.hier_code;
-    marshal_int(buf, static_cast<int64_t>(code));
+    marshal_int(buf, static_cast<int64_t>(m_data->get_hier_code()));
   }
   return INK_MIN_ALIGN;
 }
@@ -2755,17 +2683,14 @@ LogAccess::marshal_proxy_hierarchy_route(char *buf)
 int
 LogAccess::marshal_server_host_ip(char *buf)
 {
-  sockaddr const *ip = nullptr;
-  ip                 = &m_http_sm->t_state.server_info.dst_addr.sa;
-  if (!ats_is_ip(ip)) {
-    if (m_http_sm->t_state.current.server) {
-      ip = &m_http_sm->t_state.current.server->dst_addr.sa;
-      if (!ats_is_ip(ip)) {
-        ip = nullptr;
-      }
-    } else {
-      ip = nullptr;
-    }
+  sockaddr const *ip         = nullptr;
+  sockaddr const *info_dst   = m_data->get_server_info_dst_addr();
+  sockaddr const *server_dst = m_data->get_server_dst_addr();
+
+  if (info_dst && ats_is_ip(info_dst)) {
+    ip = info_dst;
+  } else if (server_dst && ats_is_ip(server_dst)) {
+    ip = server_dst;
   }
   return marshal_ip(buf, ip);
 }
@@ -2776,11 +2701,13 @@ LogAccess::marshal_server_host_ip(char *buf)
 int
 LogAccess::marshal_server_host_name(char *buf)
 {
-  char *str = nullptr;
-  int   len = INK_MIN_ALIGN;
+  const char     *str        = nullptr;
+  int             len        = INK_MIN_ALIGN;
+  sockaddr const *server_dst = m_data->get_server_dst_addr();
+  const char     *name       = m_data->get_server_name();
 
-  if (m_http_sm->t_state.current.server) {
-    str = m_http_sm->t_state.current.server->name;
+  if (server_dst && name != nullptr) {
+    str = name;
     len = LogAccess::padded_strlen(str);
   }
 
@@ -2817,7 +2744,7 @@ LogAccess::marshal_server_resp_content_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_server_response) {
-      val = m_http_sm->server_response_body_bytes;
+      val = m_data->get_server_response_body_bytes();
     }
     marshal_int(buf, val);
   }
@@ -2846,7 +2773,7 @@ LogAccess::marshal_server_resp_squid_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_server_response) {
-      val = m_server_response->length_get() + 
m_http_sm->server_response_body_bytes;
+      val = m_server_response->length_get() + 
m_data->get_server_response_body_bytes();
     }
     marshal_int(buf, val);
   }
@@ -2875,7 +2802,9 @@ int
 LogAccess::marshal_server_resp_time_ms(char *buf)
 {
   if (buf) {
-    marshal_int(buf, 
m_http_sm->milestones.difference_msec(TS_MILESTONE_SERVER_CONNECT, 
TS_MILESTONE_SERVER_CLOSE));
+    TransactionMilestones const *ms  = m_data->get_milestones();
+    int64_t                      val = ms ? 
ms->difference_msec(TS_MILESTONE_SERVER_CONNECT, TS_MILESTONE_SERVER_CLOSE) : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -2884,8 +2813,9 @@ int
 LogAccess::marshal_server_resp_time_s(char *buf)
 {
   if (buf) {
-    marshal_int(buf,
-                
static_cast<int64_t>(m_http_sm->milestones.difference_sec(TS_MILESTONE_SERVER_CONNECT,
 TS_MILESTONE_SERVER_CLOSE)));
+    TransactionMilestones const *ms = m_data->get_milestones();
+    int64_t val = ms ? 
static_cast<int64_t>(ms->difference_sec(TS_MILESTONE_SERVER_CONNECT, 
TS_MILESTONE_SERVER_CLOSE)) : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -2897,9 +2827,7 @@ int
 LogAccess::marshal_server_transact_count(char *buf)
 {
   if (buf) {
-    int64_t count;
-    count = m_http_sm->server_transact_count;
-    marshal_int(buf, count);
+    marshal_int(buf, m_data->get_server_transact_count());
   }
   return INK_MIN_ALIGN;
 }
@@ -2911,8 +2839,7 @@ int
 LogAccess::marshal_server_simple_retry_count(char *buf)
 {
   if (buf) {
-    const int64_t attempts = m_http_sm->t_state.current.simple_retry_attempts;
-    marshal_int(buf, attempts);
+    marshal_int(buf, m_data->get_simple_retry_attempts());
   }
   return INK_MIN_ALIGN;
 }
@@ -2924,8 +2851,7 @@ int
 LogAccess::marshal_server_unavailable_retry_count(char *buf)
 {
   if (buf) {
-    const int64_t attempts = 
m_http_sm->t_state.current.unavailable_server_retry_attempts;
-    marshal_int(buf, attempts);
+    marshal_int(buf, m_data->get_unavailable_retry_attempts());
   }
   return INK_MIN_ALIGN;
 }
@@ -2937,8 +2863,7 @@ int
 LogAccess::marshal_server_connect_attempts(char *buf)
 {
   if (buf) {
-    int64_t attempts = m_http_sm->t_state.current.retry_attempts.saved();
-    marshal_int(buf, attempts);
+    marshal_int(buf, m_data->get_retry_attempts_saved());
   }
   return INK_MIN_ALIGN;
 }
@@ -2970,7 +2895,7 @@ LogAccess::marshal_cache_resp_content_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_cache_response) {
-      val = m_http_sm->cache_response_body_bytes;
+      val = m_data->get_cache_response_body_bytes();
     }
     marshal_int(buf, val);
   }
@@ -2983,7 +2908,7 @@ LogAccess::marshal_cache_resp_squid_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_cache_response) {
-      val = m_cache_response->length_get() + 
m_http_sm->cache_response_body_bytes;
+      val = m_cache_response->length_get() + 
m_data->get_cache_response_body_bytes();
     }
     marshal_int(buf, val);
   }
@@ -2999,7 +2924,7 @@ LogAccess::marshal_cache_resp_header_len(char *buf)
   if (buf) {
     int64_t val = 0;
     if (m_cache_response) {
-      val = m_http_sm->cache_response_hdr_bytes;
+      val = m_data->get_cache_response_hdr_bytes();
     }
     marshal_int(buf, val);
   }
@@ -3026,51 +2951,16 @@ int
 LogAccess::marshal_client_retry_after_time(char *buf)
 {
   if (buf) {
-    int64_t crat = m_http_sm->t_state.congestion_control_crat;
-    marshal_int(buf, crat);
+    marshal_int(buf, m_data->get_congestion_control_crat());
   }
   return INK_MIN_ALIGN;
 }
 
-static LogCacheWriteCodeType
-convert_cache_write_code(HttpTransact::CacheWriteStatus_t t)
-{
-  LogCacheWriteCodeType code;
-  switch (t) {
-  case HttpTransact::CacheWriteStatus_t::NO_WRITE:
-    code = LOG_CACHE_WRITE_NONE;
-    break;
-  case HttpTransact::CacheWriteStatus_t::LOCK_MISS:
-    code = LOG_CACHE_WRITE_LOCK_MISSED;
-    break;
-  case HttpTransact::CacheWriteStatus_t::IN_PROGRESS:
-    // Hack - the HttpSM doesn't record
-    //   cache write aborts currently so
-    //   if it's not complete declare it
-    //   aborted
-    code = LOG_CACHE_WRITE_LOCK_ABORTED;
-    break;
-  case HttpTransact::CacheWriteStatus_t::ERROR:
-    code = LOG_CACHE_WRITE_ERROR;
-    break;
-  case HttpTransact::CacheWriteStatus_t::COMPLETE:
-    code = LOG_CACHE_WRITE_COMPLETE;
-    break;
-  default:
-    ink_assert(!"bad cache write code");
-    code = LOG_CACHE_WRITE_NONE;
-    break;
-  }
-
-  return code;
-}
-
 int
 LogAccess::marshal_cache_write_code(char *buf)
 {
   if (buf) {
-    int code = 
convert_cache_write_code(m_http_sm->t_state.cache_info.write_status);
-    marshal_int(buf, code);
+    marshal_int(buf, m_data->get_cache_write_code());
   }
 
   return INK_MIN_ALIGN;
@@ -3080,8 +2970,7 @@ int
 LogAccess::marshal_cache_write_transform_code(char *buf)
 {
   if (buf) {
-    int code = 
convert_cache_write_code(m_http_sm->t_state.cache_info.transform_write_status);
-    marshal_int(buf, code);
+    marshal_int(buf, m_data->get_cache_transform_write_code());
   }
 
   return INK_MIN_ALIGN;
@@ -3094,7 +2983,9 @@ int
 LogAccess::marshal_transfer_time_ms(char *buf)
 {
   if (buf) {
-    marshal_int(buf, 
m_http_sm->milestones.difference_msec(TS_MILESTONE_SM_START, 
TS_MILESTONE_SM_FINISH));
+    TransactionMilestones const *ms  = m_data->get_milestones();
+    int64_t                      val = ms ? 
ms->difference_msec(TS_MILESTONE_SM_START, TS_MILESTONE_SM_FINISH) : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -3103,7 +2994,9 @@ int
 LogAccess::marshal_transfer_time_s(char *buf)
 {
   if (buf) {
-    marshal_int(buf, 
static_cast<int64_t>(m_http_sm->milestones.difference_sec(TS_MILESTONE_SM_START,
 TS_MILESTONE_SM_FINISH)));
+    TransactionMilestones const *ms = m_data->get_milestones();
+    int64_t val = ms ? 
static_cast<int64_t>(ms->difference_sec(TS_MILESTONE_SM_START, 
TS_MILESTONE_SM_FINISH)) : 0;
+    marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
 }
@@ -3131,10 +3024,12 @@ LogAccess::marshal_file_size(char *buf)
       }
     } else {
       // This is semi-broken when we serveq zero length objects. See TS-2213
-      if (m_http_sm->server_response_body_bytes > 0) {
-        marshal_int(buf, m_http_sm->server_response_body_bytes);
-      } else if (m_http_sm->cache_response_body_bytes > 0) {
-        marshal_int(buf, m_http_sm->cache_response_body_bytes);
+      int64_t server_body = m_data->get_server_response_body_bytes();
+      int64_t cache_body  = m_data->get_cache_response_body_bytes();
+      if (server_body > 0) {
+        marshal_int(buf, server_body);
+      } else if (cache_body > 0) {
+        marshal_int(buf, cache_body);
       }
     }
   }
@@ -3150,11 +3045,7 @@ int
 LogAccess::marshal_client_http_connection_id(char *buf)
 {
   if (buf) {
-    int64_t id = 0;
-    if (m_http_sm) {
-      id = m_http_sm->client_connection_id();
-    }
-    marshal_int(buf, id);
+    marshal_int(buf, m_data->get_connection_id());
   }
   return INK_MIN_ALIGN;
 }
@@ -3166,11 +3057,7 @@ int
 LogAccess::marshal_client_http_transaction_id(char *buf)
 {
   if (buf) {
-    int64_t id = 0;
-    if (m_http_sm) {
-      id = m_http_sm->client_transaction_id();
-    }
-    marshal_int(buf, id);
+    marshal_int(buf, m_data->get_transaction_id());
   }
   return INK_MIN_ALIGN;
 }
@@ -3182,11 +3069,7 @@ int
 LogAccess::marshal_client_http_transaction_priority_weight(char *buf)
 {
   if (buf) {
-    int64_t id = 0;
-    if (m_http_sm) {
-      id = m_http_sm->client_transaction_priority_weight();
-    }
-    marshal_int(buf, id);
+    marshal_int(buf, m_data->get_transaction_priority_weight());
   }
   return INK_MIN_ALIGN;
 }
@@ -3198,11 +3081,7 @@ int
 LogAccess::marshal_client_http_transaction_priority_dependence(char *buf)
 {
   if (buf) {
-    int64_t id = 0;
-    if (m_http_sm) {
-      id = m_http_sm->client_transaction_priority_dependence();
-    }
-    marshal_int(buf, id);
+    marshal_int(buf, m_data->get_transaction_priority_dependence());
   }
   return INK_MIN_ALIGN;
 }
@@ -3214,11 +3093,7 @@ int
 LogAccess::marshal_cache_read_retries(char *buf)
 {
   if (buf) {
-    int64_t id = 0;
-    if (m_http_sm) {
-      id = m_http_sm->get_cache_sm().get_open_read_tries();
-    }
-    marshal_int(buf, id);
+    marshal_int(buf, m_data->get_cache_open_read_tries());
   }
   return INK_MIN_ALIGN;
 }
@@ -3230,11 +3105,7 @@ int
 LogAccess::marshal_cache_write_retries(char *buf)
 {
   if (buf) {
-    int64_t id = 0;
-    if (m_http_sm) {
-      id = m_http_sm->get_cache_sm().get_open_write_tries();
-    }
-    marshal_int(buf, id);
+    marshal_int(buf, m_data->get_cache_open_write_tries());
   }
   return INK_MIN_ALIGN;
 }
@@ -3243,20 +3114,16 @@ int
 LogAccess::marshal_cache_collapsed_connection_success(char *buf)
 {
   if (buf) {
-    int64_t id = 0; // default - no collapse attempt
-    if (m_http_sm) {
-      SquidLogCode code = m_http_sm->t_state.squid_codes.log_code;
+    int64_t      id                = 0; // default - no collapse attempt
+    SquidLogCode code              = m_data->get_log_code();
+    int          open_write_tries  = m_data->get_cache_open_write_tries();
+    int          max_write_retries = 
m_data->get_max_cache_open_write_retries();
 
-      // We attempted an open write, but ended up with some sort of HIT which 
means we must have gone back to the read state
-      if ((m_http_sm->get_cache_sm().get_open_write_tries() > (0)) &&
-          ((code == SquidLogCode::TCP_HIT) || (code == 
SquidLogCode::TCP_MEM_HIT) || (code == SquidLogCode::TCP_DISK_HIT) ||
-           (code == SquidLogCode::TCP_CF_HIT))) {
-        // Attempted collapsed connection and got a hit, success
-        id = 1;
-      } else if (m_http_sm->get_cache_sm().get_open_write_tries() > 
(m_http_sm->t_state.txn_conf->max_cache_open_write_retries)) {
-        // Attempted collapsed connection with no hit, failure, we can also 
get +2 retries in a failure state
-        id = -1;
-      }
+    if ((open_write_tries > 0) && ((code == SquidLogCode::TCP_HIT) || (code == 
SquidLogCode::TCP_MEM_HIT) ||
+                                   (code == SquidLogCode::TCP_DISK_HIT) || 
(code == SquidLogCode::TCP_CF_HIT))) {
+      id = 1;
+    } else if (max_write_retries >= 0 && open_write_tries > max_write_retries) 
{
+      id = -1;
     }
 
     marshal_int(buf, id);
@@ -3335,8 +3202,9 @@ LogAccess::marshal_http_header_field(LogField::Container 
container, char *field,
 
   // The Transfer-Encoding:chunked value may have been removed from the header
   // during processing, but we store it for logging purposes.
-  if (valid_field == false && container == LogField::SSH && strcmp(field, 
"Transfer-Encoding") == 0) {
-    const std::string &stored_te = 
m_http_sm->t_state.hdr_info.server_response_transfer_encoding;
+  std::string_view transfer_enc = 
m_data->get_server_response_transfer_encoding();
+  if (valid_field == false && !transfer_enc.empty() && container == 
LogField::SSH && strcmp(field, "Transfer-Encoding") == 0) {
+    std::string_view stored_te = transfer_enc;
     if (!stored_te.empty()) {
       valid_field = true;
       actual_len  = stored_te.length();
@@ -3454,7 +3322,8 @@ int
 LogAccess::marshal_milestone(TSMilestonesType ms, char *buf)
 {
   if (buf) {
-    int64_t val = ink_hrtime_to_msec(m_http_sm->milestones[ms]);
+    TransactionMilestones const *milestones = m_data->get_milestones();
+    int64_t                      val        = milestones ? 
ink_hrtime_to_msec((*milestones)[ms]) : 0;
     marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
@@ -3464,7 +3333,8 @@ int
 LogAccess::marshal_milestone_fmt_sec(TSMilestonesType type, char *buf)
 {
   if (buf) {
-    ink_hrtime tsec = ink_hrtime_to_sec(m_http_sm->milestones[type]);
+    TransactionMilestones const *milestones = m_data->get_milestones();
+    ink_hrtime                   tsec       = milestones ? 
ink_hrtime_to_sec((*milestones)[type]) : 0;
     marshal_int(buf, tsec);
   }
   return INK_MIN_ALIGN;
@@ -3474,7 +3344,8 @@ int
 LogAccess::marshal_milestone_fmt_ms(TSMilestonesType type, char *buf)
 {
   if (buf) {
-    ink_hrtime tmsec = ink_hrtime_to_msec(m_http_sm->milestones[type]);
+    TransactionMilestones const *milestones = m_data->get_milestones();
+    ink_hrtime                   tmsec      = milestones ? 
ink_hrtime_to_msec((*milestones)[type]) : 0;
     marshal_int(buf, tmsec);
   }
   return INK_MIN_ALIGN;
@@ -3484,7 +3355,8 @@ int
 LogAccess::marshal_milestone_diff(TSMilestonesType ms1, TSMilestonesType ms2, 
char *buf)
 {
   if (buf) {
-    int64_t val = m_http_sm->milestones.difference_msec(ms2, ms1);
+    TransactionMilestones const *milestones = m_data->get_milestones();
+    int64_t                      val        = milestones ? 
milestones->difference_msec(ms2, ms1) : 0;
     marshal_int(buf, val);
   }
   return INK_MIN_ALIGN;
diff --git a/src/proxy/logging/unit-tests/test_LogAccess.cc 
b/src/proxy/logging/unit-tests/test_LogAccess.cc
new file mode 100644
index 0000000000..e567a2889c
--- /dev/null
+++ b/src/proxy/logging/unit-tests/test_LogAccess.cc
@@ -0,0 +1,211 @@
+/** @file
+
+  Unit tests for LogAccess.
+
+  @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 <catch2/catch_test_macros.hpp>
+
+#include "proxy/PreTransactionLogData.h"
+#include "proxy/logging/LogAccess.h"
+#include "tscore/ink_inet.h"
+
+#include <string_view>
+#include <vector>
+
+using namespace std::literals;
+
+extern int cmd_disable_pfreelist;
+
+namespace
+{
+void
+initialize_headers_once()
+{
+  static bool initialized = false;
+
+  if (!initialized) {
+    cmd_disable_pfreelist = true;
+    url_init();
+    mime_init();
+    http_init();
+    initialized = true;
+  }
+}
+
+void
+add_header_field(HTTPHdr &hdr, std::string_view name, std::string_view value)
+{
+  MIMEField *field = hdr.field_create(name);
+  REQUIRE(field != nullptr);
+  field->value_set(hdr.m_heap, hdr.m_mime, value);
+  hdr.field_attach(field);
+}
+
+std::string
+synthesize_target(std::string_view method, std::string_view scheme, 
std::string_view authority, std::string_view path)
+{
+  if (method == static_cast<std::string_view>(HTTP_METHOD_CONNECT)) {
+    if (!authority.empty()) {
+      return std::string(authority);
+    }
+    if (!path.empty()) {
+      return std::string(path);
+    }
+    return {};
+  }
+
+  if (!scheme.empty() && !authority.empty()) {
+    std::string url;
+    url.reserve(scheme.size() + authority.size() + path.size() + 4);
+    url.append(scheme);
+    url.append("://");
+    url.append(authority);
+    if (!path.empty()) {
+      url.append(path);
+    } else {
+      url.push_back('/');
+    }
+    return url;
+  }
+
+  if (!path.empty()) {
+    return std::string(path);
+  }
+
+  return authority.empty() ? std::string{} : std::string(authority);
+}
+
+void
+set_socket_address(IpEndpoint &ep, std::string_view text)
+{
+  REQUIRE(0 == ats_ip_pton(text, &ep.sa));
+}
+
+void
+populate_pre_transaction_data(PreTransactionLogData &data, std::string_view 
method, std::string_view scheme,
+                              std::string_view authority, std::string_view 
path)
+{
+  initialize_headers_once();
+
+  auto *heap = new_HdrHeap(HdrHeap::DEFAULT_SIZE + 64);
+  data.owned_client_request.create(HTTPType::REQUEST, HTTP_2_0, heap);
+  data.owned_client_request.method_set(method);
+  data.m_client_connection_is_ssl = true;
+  data.m_log_code                 = SquidLogCode::ERR_INVALID_REQ;
+  data.m_hit_miss_code            = SQUID_MISS_NONE;
+  data.m_hier_code                = SquidHierarchyCode::NONE;
+  data.m_server_transact_count    = 0;
+  data.owned_client_protocol_str  = "http/2";
+  data.owned_method.assign(method.data(), method.size());
+  data.owned_scheme.assign(scheme.data(), scheme.size());
+  data.owned_authority.assign(authority.data(), authority.size());
+  data.owned_path.assign(path.data(), path.size());
+  data.owned_url = synthesize_target(method, scheme, authority, path);
+  set_socket_address(data.owned_client_addr, "192.0.2.10:4321"sv);
+  ats_ip_copy(&data.owned_client_src_addr.sa, &data.owned_client_addr.sa);
+  data.m_client_port = ats_ip_port_host_order(&data.owned_client_addr.sa);
+
+  add_header_field(data.owned_client_request, PSEUDO_HEADER_METHOD, method);
+  if (!scheme.empty()) {
+    add_header_field(data.owned_client_request, PSEUDO_HEADER_SCHEME, scheme);
+  }
+  if (!authority.empty()) {
+    add_header_field(data.owned_client_request, PSEUDO_HEADER_AUTHORITY, 
authority);
+  }
+  if (!path.empty()) {
+    add_header_field(data.owned_client_request, PSEUDO_HEADER_PATH, path);
+  }
+  add_header_field(data.owned_client_request, 
static_cast<std::string_view>(MIME_FIELD_USER_AGENT), "TikTok/1.0");
+
+  data.owned_milestones[TS_MILESTONE_SM_START]            = 
ink_hrtime_from_msec(10);
+  data.owned_milestones[TS_MILESTONE_UA_BEGIN]            = 
ink_hrtime_from_msec(10);
+  data.owned_milestones[TS_MILESTONE_UA_FIRST_READ]       = 
ink_hrtime_from_msec(12);
+  data.owned_milestones[TS_MILESTONE_UA_READ_HEADER_DONE] = 
ink_hrtime_from_msec(14);
+  data.owned_milestones[TS_MILESTONE_SM_FINISH]           = 
ink_hrtime_from_msec(15);
+}
+
+template <typename Marshal>
+std::string
+marshal_string(Marshal marshal)
+{
+  const int         len = marshal(nullptr);
+  std::vector<char> buffer(len);
+  marshal(buffer.data());
+  return std::string(buffer.data());
+}
+
+template <typename Marshal>
+int64_t
+marshal_int_value(Marshal marshal)
+{
+  std::vector<char> buffer(INK_MIN_ALIGN * 2);
+  marshal(buffer.data());
+  char *ptr = buffer.data();
+  return LogAccess::unmarshal_int(&ptr);
+}
+} // namespace
+
+TEST_CASE("LogAccess pre-transaction CONNECT fields", "[LogAccess]")
+{
+  PreTransactionLogData data;
+  populate_pre_transaction_data(data, "CONNECT", ""sv, "example.com:443", 
""sv);
+  LogAccess access(data);
+
+  access.init();
+
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_client_req_http_method(buf); }) == "CONNECT");
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_client_req_protocol_version(buf); }) == "http/2");
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_client_req_url(buf); }) == "example.com:443");
+  CHECK(marshal_int_value([&](char *buf) { return 
access.marshal_cache_result_code(buf); }) ==
+        static_cast<int64_t>(SquidLogCode::ERR_INVALID_REQ));
+  CHECK(marshal_int_value([&](char *buf) { return 
access.marshal_server_transact_count(buf); }) == 0);
+
+  char user_agent[] = "User-Agent";
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_http_header_field(LogField::CQH, user_agent, buf); }) ==
+        "TikTok/1.0");
+}
+
+TEST_CASE("LogAccess malformed CONNECT without authority falls back to path", 
"[LogAccess]")
+{
+  PreTransactionLogData data;
+  populate_pre_transaction_data(data, "CONNECT", "https"sv, ""sv, "/"sv);
+  LogAccess access(data);
+
+  access.init();
+
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_client_req_url(buf); }) == "/");
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_client_req_url_canon(buf); }) == "/");
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_client_req_url_path(buf); }) == "/");
+  CHECK(marshal_string([&](char *buf) { return 
access.marshal_client_req_url_scheme(buf); }) == "https");
+  CHECK(marshal_int_value([&](char *buf) { return 
access.marshal_transfer_time_ms(buf); }) == 5);
+}
+
+TEST_CASE("LogAccess pre-transaction client host port is null-safe", 
"[LogAccess]")
+{
+  PreTransactionLogData data;
+  populate_pre_transaction_data(data, "GET", "https"sv, "example.com", 
"/client-port"sv);
+  LogAccess access(data);
+
+  access.init();
+
+  CHECK(access.marshal_client_host_port(nullptr) == INK_MIN_ALIGN);
+  CHECK(marshal_int_value([&](char *buf) { return 
access.marshal_client_host_port(buf); }) == 4321);
+}
diff --git a/tests/gold_tests/connect/h2_malformed_request_logging.test.py 
b/tests/gold_tests/connect/h2_malformed_request_logging.test.py
new file mode 100644
index 0000000000..7974967789
--- /dev/null
+++ b/tests/gold_tests/connect/h2_malformed_request_logging.test.py
@@ -0,0 +1,195 @@
+'''
+Verify malformed HTTP/2 requests are access logged.
+'''
+#  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.
+
+import os
+import re
+import sys
+
+Test.Summary = 'Malformed HTTP/2 requests are logged before transaction 
creation'
+
+
+class MalformedH2RequestLoggingTest:
+    """
+    Exercise malformed and valid HTTP/2 request logging paths.
+    """
+
+    REPLAY_FILE = 'replays/h2_malformed_request_logging.replay.yaml'
+    MALFORMED_CLIENT = 'malformed_h2_request_client.py'
+    MALFORMED_CASES = (
+        {
+            'scenario': 'connect-missing-authority',
+            'uuid': 'malformed-connect',
+            'method': 'CONNECT',
+            'pqu': '/',
+            'description': 'Send malformed HTTP/2 CONNECT request',
+        },
+        {
+            'scenario': 'get-missing-path',
+            'uuid': 'malformed-get-missing-path',
+            'method': 'GET',
+            'pqu': 'https://missing-path.example/',
+            'description': 'Send malformed HTTP/2 GET request without :path',
+        },
+        {
+            'scenario': 'get-connection-header',
+            'uuid': 'malformed-get-connection',
+            'method': 'GET',
+            'pqu': 'https://bad-connection.example/bad-connection',
+            'description': 'Send malformed HTTP/2 GET request with Connection 
header',
+        },
+    )
+
+    def __init__(self):
+        self._setup_server()
+        self._setup_ts()
+        self._processes_started = False
+        Test.Setup.CopyAs(self.MALFORMED_CLIENT, Test.RunDirectory)
+
+    @property
+    def _squid_log_path(self) -> str:
+        return os.path.join(self._ts.Variables.LOGDIR, 'squid.log')
+
+    def _setup_server(self):
+        self._server = 
Test.MakeVerifierServerProcess('malformed-request-server', self.REPLAY_FILE)
+        for case in self.MALFORMED_CASES:
+            self._server.Streams.stdout += Testers.ExcludesExpression(
+                f'uuid: {case["uuid"]}',
+                f'{case["description"]} must not reach the origin server.',
+            )
+        self._server.Streams.stdout += Testers.ContainsExpression(
+            'GET /get HTTP/1.1\nuuid: valid-connect',
+            reflags=re.MULTILINE,
+            description='A valid CONNECT tunnel should still reach the 
origin.',
+        )
+        self._server.Streams.stdout += Testers.ContainsExpression(
+            r'GET /valid-get HTTP/1\.1\n(?:.*\n)*uuid: valid-get',
+            reflags=re.MULTILINE,
+            description='A valid non-CONNECT request should still reach the 
origin.',
+        )
+
+    def _setup_ts(self):
+        self._ts = Test.MakeATSProcess('ts', enable_tls=True, 
enable_cache=False)
+        self._ts.addDefaultSSLFiles()
+        self._ts.Disk.File(
+            os.path.join(self._ts.Variables.CONFIGDIR, 'storage.config'),
+            id='storage_config',
+            typename='ats:config',
+        )
+        self._ts.Disk.storage_config.AddLine('')
+        self._ts.Disk.ssl_multicert_config.AddLine('ssl_cert_name=server.pem 
ssl_key_name=server.key dest_ip=*')
+
+        self._ts.Disk.records_config.update(
+            {
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'http|hpack|http2',
+                'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir,
+                'proxy.config.ssl.server.private_key.path': 
self._ts.Variables.SSLDir,
+                'proxy.config.http.server_ports': 
f'{self._ts.Variables.ssl_port}:ssl',
+                'proxy.config.http.connect_ports': 
self._server.Variables.http_port,
+            })
+        self._ts.Disk.remap_config.AddLine(f'map / 
http://127.0.0.1:{self._server.Variables.http_port}/')
+        self._ts.Disk.logging_yaml.AddLines(
+            """
+logging:
+  formats:
+    - name: malformed_h2_request
+      format: 'uuid=%<{uuid}cqh> cqpv=%<cqpv> cqhm=%<cqhm> crc=%<crc> 
sstc=%<sstc> pqu=%<pqu>'
+  logs:
+    - filename: squid
+      format: malformed_h2_request
+      mode: ascii
+""".split('\n'))
+        self._ts.Disk.diags_log.Content = Testers.ContainsExpression(
+            'recv headers malformed request',
+            'ATS should reject malformed requests at the HTTP/2 layer.',
+        )
+        for index, case in enumerate(self.MALFORMED_CASES):
+            expected = (
+                rf'uuid={case["uuid"]} cqpv=http/2 cqhm={case["method"]} '
+                rf'crc=ERR_INVALID_REQ sstc=0 pqu={re.escape(case["pqu"])}')
+            tester = Testers.ContainsExpression(
+                expected,
+                f'{case["description"]} should be logged with 
ERR_INVALID_REQ.',
+            )
+            if index == 0:
+                self._ts.Disk.squid_log.Content = tester
+            else:
+                self._ts.Disk.squid_log.Content += tester
+        self._ts.Disk.squid_log.Content += Testers.ContainsExpression(
+            r'uuid=valid-connect cqpv=http/2 cqhm=CONNECT ',
+            'A valid HTTP/2 CONNECT should still use the normal transaction 
log path.',
+        )
+        self._ts.Disk.squid_log.Content += Testers.ContainsExpression(
+            r'uuid=valid-get cqpv=http/2 cqhm=GET ',
+            'A valid HTTP/2 GET should still use the normal transaction log 
path.',
+        )
+        self._ts.Disk.squid_log.Content += Testers.ExcludesExpression(
+            r'uuid=valid-connect .*crc=ERR_INVALID_REQ',
+            'Valid HTTP/2 CONNECT logging must not be marked as malformed.',
+        )
+        self._ts.Disk.squid_log.Content += Testers.ExcludesExpression(
+            r'uuid=valid-get .*crc=ERR_INVALID_REQ',
+            'Valid HTTP/2 GET logging must not be marked as malformed.',
+        )
+
+    def _add_malformed_request_runs(self):
+        for case in self.MALFORMED_CASES:
+            tr = Test.AddTestRun(case['description'])
+            tr.Processes.Default.Command = (
+                f'{sys.executable} {self.MALFORMED_CLIENT} 
{self._ts.Variables.ssl_port} {case["scenario"]}')
+            tr.Processes.Default.ReturnCode = 0
+            self._keep_support_processes_running(tr)
+            tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+                r'Received (RST_STREAM on stream 1 with error code 1|GOAWAY 
with error code [01])',
+                'ATS should reject the malformed request at the HTTP/2 layer.',
+            )
+
+    def _add_valid_request_run(self):
+        tr = Test.AddTestRun('Send valid HTTP/2 requests')
+        tr.AddVerifierClientProcess('valid-request-client', self.REPLAY_FILE, 
https_ports=[self._ts.Variables.ssl_port])
+        self._keep_support_processes_running(tr)
+
+    def _await_malformed_log_entries(self):
+        tr = Test.AddAwaitFileContainsTestRun(
+            'Await malformed request squid log entries',
+            self._squid_log_path,
+            'crc=ERR_INVALID_REQ',
+            desired_count=len(self.MALFORMED_CASES),
+        )
+        self._keep_support_processes_running(tr)
+
+    def _keep_support_processes_running(self, tr):
+        if self._processes_started:
+            tr.StillRunningAfter = self._server
+            tr.StillRunningAfter = self._ts
+            return
+
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts)
+        tr.StillRunningAfter = self._server
+        tr.StillRunningAfter = self._ts
+        self._processes_started = True
+
+    def run(self):
+        self._add_malformed_request_runs()
+        self._add_valid_request_run()
+        self._await_malformed_log_entries()
+
+
+MalformedH2RequestLoggingTest().run()
diff --git a/tests/gold_tests/connect/malformed_h2_request_client.py 
b/tests/gold_tests/connect/malformed_h2_request_client.py
new file mode 100644
index 0000000000..c60178287c
--- /dev/null
+++ b/tests/gold_tests/connect/malformed_h2_request_client.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3
+
+#  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.
+"""
+Send malformed HTTP/2 requests directly on the wire.
+"""
+
+import argparse
+import socket
+import ssl
+import sys
+
+import hpack
+
+H2_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
+
+TYPE_HEADERS = 0x01
+TYPE_RST_STREAM = 0x03
+TYPE_SETTINGS = 0x04
+TYPE_GOAWAY = 0x07
+
+FLAG_ACK = 0x01
+FLAG_END_STREAM = 0x01
+FLAG_END_HEADERS = 0x04
+
+PROTOCOL_ERROR = 0x01
+
+
+def make_frame(frame_type: int, flags: int, stream_id: int, payload: bytes = 
b"") -> bytes:
+    return (len(payload).to_bytes(3, "big") + bytes([frame_type, flags]) + 
(stream_id & 0x7FFFFFFF).to_bytes(4, "big") + payload)
+
+
+def recv_exact(sock: socket.socket, size: int) -> bytes:
+    data = bytearray()
+    while len(data) < size:
+        chunk = sock.recv(size - len(data))
+        if not chunk:
+            break
+        data.extend(chunk)
+    return bytes(data)
+
+
+def read_frame(sock: socket.socket):
+    header = recv_exact(sock, 9)
+    if len(header) == 0:
+        return None
+    if len(header) != 9:
+        raise RuntimeError(f"incomplete frame header: got {len(header)} bytes")
+
+    length = int.from_bytes(header[0:3], "big")
+    payload = recv_exact(sock, length)
+    if len(payload) != length:
+        raise RuntimeError(f"incomplete frame payload: expected {length}, got 
{len(payload)}")
+
+    return {
+        "length": length,
+        "type": header[3],
+        "flags": header[4],
+        "stream_id": int.from_bytes(header[5:9], "big") & 0x7FFFFFFF,
+        "payload": payload,
+    }
+
+
+def connect_socket(port: int) -> socket.socket:
+    socket.setdefaulttimeout(5)
+
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    ctx.set_alpn_protocols(["h2"])
+
+    tls_socket = socket.create_connection(("127.0.0.1", port))
+    tls_socket = ctx.wrap_socket(tls_socket, server_hostname="localhost")
+    if tls_socket.selected_alpn_protocol() != "h2":
+        raise RuntimeError(f"failed to negotiate h2, got 
{tls_socket.selected_alpn_protocol()!r}")
+    return tls_socket
+
+
+def make_malformed_headers(scenario: str) -> bytes:
+    encoder = hpack.Encoder()
+    if scenario == "connect-missing-authority":
+        headers = [
+            (":method", "CONNECT"),
+            (":scheme", "https"),
+            (":path", "/"),
+            ("user-agent", "TikTok/1.0"),
+            ("uuid", "malformed-connect"),
+        ]
+    elif scenario == "get-missing-path":
+        headers = [
+            (":method", "GET"),
+            (":scheme", "https"),
+            (":authority", "missing-path.example"),
+            ("user-agent", "Malformed/1.0"),
+            ("uuid", "malformed-get-missing-path"),
+        ]
+    elif scenario == "get-connection-header":
+        headers = [
+            (":method", "GET"),
+            (":scheme", "https"),
+            (":authority", "bad-connection.example"),
+            (":path", "/bad-connection"),
+            ("connection", "close"),
+            ("user-agent", "Malformed/1.0"),
+            ("uuid", "malformed-get-connection"),
+        ]
+    else:
+        raise ValueError(f"unknown scenario: {scenario}")
+
+    return encoder.encode(headers)
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser()
+    parser.add_argument("port", type=int, help="TLS port to connect to")
+    parser.add_argument(
+        "scenario",
+        choices=("connect-missing-authority", "get-missing-path", 
"get-connection-header"),
+        help="Malformed request shape to send",
+    )
+    args = parser.parse_args()
+
+    tls_socket = connect_socket(args.port)
+    try:
+        payload = make_malformed_headers(args.scenario)
+        tls_socket.sendall(H2_PREFACE)
+        tls_socket.sendall(make_frame(TYPE_SETTINGS, 0, 0))
+        tls_socket.sendall(make_frame(TYPE_HEADERS, FLAG_END_STREAM | 
FLAG_END_HEADERS, 1, payload))
+
+        while True:
+            frame = read_frame(tls_socket)
+            if frame is None:
+                print(f"Connection closed after malformed request scenario 
{args.scenario}")
+                return 0
+
+            frame_type = frame["type"]
+            if frame_type == TYPE_SETTINGS and not (frame["flags"] & FLAG_ACK):
+                tls_socket.sendall(make_frame(TYPE_SETTINGS, FLAG_ACK, 0))
+                continue
+
+            if frame_type == TYPE_RST_STREAM and frame["stream_id"] == 1:
+                error_code = int.from_bytes(frame["payload"][0:4], "big")
+                print(f"Received RST_STREAM on stream 1 with error code 
{error_code}")
+                return 0 if error_code == PROTOCOL_ERROR else 1
+
+            if frame_type == TYPE_GOAWAY:
+                error_code = int.from_bytes(frame["payload"][4:8], "big")
+                print(f"Received GOAWAY with error code {error_code}")
+                return 0 if error_code in (0, PROTOCOL_ERROR) else 1
+    except socket.timeout:
+        print(f"Timed out waiting for ATS to reject malformed request scenario 
{args.scenario}", file=sys.stderr)
+        return 1
+    finally:
+        tls_socket.close()
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git 
a/tests/gold_tests/connect/replays/h2_malformed_request_logging.replay.yaml 
b/tests/gold_tests/connect/replays/h2_malformed_request_logging.replay.yaml
new file mode 100644
index 0000000000..fdb1169153
--- /dev/null
+++ b/tests/gold_tests/connect/replays/h2_malformed_request_logging.replay.yaml
@@ -0,0 +1,90 @@
+#  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.
+
+# This replays healthy HTTP/2 requests to verify their logging.
+
+meta:
+  version: "1.0"
+
+sessions:
+  - protocol:
+      stack: http2
+      tls:
+        sni: www.example.com
+    transactions:
+      - client-request:
+          headers:
+            encoding: esc_json
+            fields:
+              - [ :method, GET ]
+              - [ :scheme, https ]
+              - [ :authority, www.example.com ]
+              - [ :path, /valid-get ]
+              - [ uuid, valid-get ]
+          content:
+            encoding: plain
+            size: 0
+
+        proxy-request:
+          method: GET
+
+        server-response:
+          status: 200
+          reason: OK
+          content:
+            encoding: plain
+            data: response_to_valid_get
+            size: 21
+
+        proxy-response:
+          status: 200
+          content:
+            verify: { value: "response_to_valid_get", as: contains }
+  - protocol:
+      stack: http2
+      tls:
+        sni: www.example.com
+    transactions:
+      - client-request:
+          frames:
+            - HEADERS:
+                headers:
+                  fields:
+                    - [:method, CONNECT]
+                    - [:authority, www.example.com:80]
+                    - [uuid, valid-connect]
+                    - [test, connect-request]
+            - DATA:
+                content:
+                  encoding: plain
+                  data: "GET /get HTTP/1.1\r\nuuid: valid-connect\r\ntest: 
real-request\r\n\r\n"
+
+        # Note: the server will receive the tunneled GET request.
+        proxy-request:
+          method: GET
+
+        server-response:
+          status: 200
+          reason: OK
+          content:
+            encoding: plain
+            data: response_to_tunnelled_request
+            size: 29
+
+        proxy-response:
+          status: 200
+          content:
+            verify: { value: "response_to_tunnelled_request", as: contains }


Reply via email to