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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2119ac95f4 Compact response HTTP version log fields (#13568)
2119ac95f4 is described below

commit 2119ac95f4e745e716aace2fdb105743344c2189
Author: Brian Neradt <[email protected]>
AuthorDate: Wed Aug 19 12:44:31 2026 -0500

    Compact response HTTP version log fields (#13568)
    
    The sshv and csshv access log fields store two one-byte version
    components as separate 64-bit integers, consuming 16 bytes per entry and
    publishing dINT framing in binary log schemas.
    
    This patch addresses the overhead by marshaling major.minor strings in
    one 8-byte-aligned slot while preserving the existing HTTP/major.minor
    ASCII form. It updates v3 schema expectations and regression coverage
    for both fields.
    
    Fixes: #12891
---
 .../binary-log-v3-format.en.rst                    | 16 ++---
 include/proxy/logging/LogAccess.h                  |  4 +-
 include/proxy/logging/LogField.h                   |  2 +-
 src/proxy/logging/Log.cc                           |  4 +-
 src/proxy/logging/LogAccess.cc                     | 80 ++++++++--------------
 src/proxy/logging/unit-tests/test_LogAccess.cc     | 30 +++++---
 src/proxy/logging/unit-tests/test_LogBuffer.cc     | 11 +--
 src/traffic_logcat/unit-tests/test_LogEntryJson.cc |  6 +-
 tests/gold_tests/logging/binary_log_v3.test.py     |  4 +-
 .../logging/gold/binary_log_v3_json.gold           |  6 +-
 10 files changed, 74 insertions(+), 89 deletions(-)

diff --git 
a/doc/developer-guide/logging-architecture/binary-log-v3-format.en.rst 
b/doc/developer-guide/logging-architecture/binary-log-v3-format.en.rst
index 7deb532ae1..fca1247ca8 100644
--- a/doc/developer-guide/logging-architecture/binary-log-v3-format.en.rst
+++ b/doc/developer-guide/logging-architecture/binary-log-v3-format.en.rst
@@ -104,9 +104,8 @@ Code Name      Wire encoding
                meets it -- or any code it does not recognize -- cannot
                determine the field length and must stop decoding the entry.
 1    sINT      A single ``int64_t``, fixed 8 bytes, **host byte order**.
-2    dINT      Two ``int64_t`` (16 bytes), host byte order. Used for
-               values stored as two integers, e.g. HTTP version
-               major/minor.
+2    dINT      Two ``int64_t`` (16 bytes), host byte order. Used for values
+               stored as two integers.
 3    STRING    NUL-terminated bytes, then padded to an 8-byte boundary.
 4    IP        ``uint16_t`` address family followed by a family-sized
                address, then padded to an 8-byte boundary (see below).
@@ -116,8 +115,8 @@ The code reflects how the value is *framed* on disk, i.e. 
how a reader walks
 (or skips) it -- not what the value means. (The ``sINT``/``dINT`` names are an
 ATS-internal distinction; on the wire ``sINT`` is one 8-byte integer and
 ``dINT`` is two consecutive ones.) How a consumer *renders* a value -- mapping
-a cache-result integer to ``TCP_HIT``, or a ``dINT`` to ``1.1`` -- is layered
-on top by the consumer and is not part of the wire format.
+a cache-result integer to ``TCP_HIT``, for example -- is layered on top by the
+consumer and is not part of the wire format.
 
 Value encodings
 ===============
@@ -129,10 +128,9 @@ sINT
     portability is future work.
 
 dINT
-    Two consecutive ``sINT`` values: 16 bytes total, in host byte order. Used
-    where one log field is stored as two integers, such as an HTTP version
-    (major then minor). The reference decoder renders it as a JSON array, e.g.
-    ``[1,1]``; turning that into ``1.1`` is a consumer concern.
+    Two consecutive ``sINT`` values: 16 bytes total, in host byte order. The
+    reference decoder renders them as a JSON array, e.g. ``[1,1]``; 
interpreting
+    that pair is a consumer concern.
 
 STRING
     The string bytes followed by a single NUL, then zero padding up to the next
diff --git a/include/proxy/logging/LogAccess.h 
b/include/proxy/logging/LogAccess.h
index 35f14ea55c..ea7a2aa3fa 100644
--- a/include/proxy/logging/LogAccess.h
+++ b/include/proxy/logging/LogAccess.h
@@ -224,7 +224,7 @@ public:
   int marshal_server_resp_squid_len(char *);          // INT
   int marshal_server_resp_content_len(char *);        // INT
   int marshal_server_resp_header_len(char *);         // INT
-  int marshal_server_resp_http_version(char *);       // INT
+  int marshal_server_resp_http_version(char *);       // STR
   int marshal_server_resp_time_ms(char *);            // INT
   int marshal_server_resp_time_s(char *);             // INT
   int marshal_server_transact_count(char *);          // INT
@@ -240,7 +240,7 @@ public:
   int marshal_cache_resp_squid_len(char *);         // INT
   int marshal_cache_resp_content_len(char *);       // INT
   int marshal_cache_resp_header_len(char *);        // INT
-  int marshal_cache_resp_http_version(char *);      // INT
+  int marshal_cache_resp_http_version(char *);      // STR
   int marshal_cache_resp_all_header_fields(char *); // STR
 
   void set_client_req_url(char *, int);                // STR
diff --git a/include/proxy/logging/LogField.h b/include/proxy/logging/LogField.h
index 1fbb3dc216..819889477b 100644
--- a/include/proxy/logging/LogField.h
+++ b/include/proxy/logging/LogField.h
@@ -107,7 +107,7 @@ public:
   enum class Type : uint8_t {
     INVALID = 0, ///< Reserved: never written; a reader treats 0 (or any 
unknown code) as unframmable and stops.
     sINT    = 1, ///< one int64_t, 8 bytes (host byte order).
-    dINT    = 2, ///< two int64_t (16 bytes), e.g. HTTP version major/minor.
+    dINT    = 2, ///< two int64_t (16 bytes).
     STRING  = 3, ///< NUL-terminated, 8-byte padded.
     IP      = 4, ///< uint16_t family + family-sized address, 8-byte padded.
     N_TYPES = 5, ///< Internal bound (asserts / name table); NOT a wire code.
diff --git a/src/proxy/logging/Log.cc b/src/proxy/logging/Log.cc
index 6e72ac408b..e87b345321 100644
--- a/src/proxy/logging/Log.cc
+++ b/src/proxy/logging/Log.cc
@@ -900,7 +900,7 @@ Log::init_fields()
   global_field_list.add(field, false);
   field_symbol_hash.emplace("ssql", field);
 
-  field = new LogField("server_resp_http_version", "sshv", 
LogField::Type::dINT, &LogAccess::marshal_server_resp_http_version,
+  field = new LogField("server_resp_http_version", "sshv", 
LogField::Type::STRING, &LogAccess::marshal_server_resp_http_version,
                        &LogAccess::unmarshal_http_version);
   global_field_list.add(field, false);
   field_symbol_hash.emplace("sshv", field);
@@ -975,7 +975,7 @@ Log::init_fields()
   global_field_list.add(field, false);
   field_symbol_hash.emplace("cssql", field);
 
-  field = new LogField("cached_resp_http_version", "csshv", 
LogField::Type::dINT, &LogAccess::marshal_cache_resp_http_version,
+  field = new LogField("cached_resp_http_version", "csshv", 
LogField::Type::STRING, &LogAccess::marshal_cache_resp_http_version,
                        &LogAccess::unmarshal_http_version);
   global_field_list.add(field, false);
   field_symbol_hash.emplace("csshv", field);
diff --git a/src/proxy/logging/LogAccess.cc b/src/proxy/logging/LogAccess.cc
index 6d0d2fe664..1114af1b05 100644
--- a/src/proxy/logging/LogAccess.cc
+++ b/src/proxy/logging/LogAccess.cc
@@ -53,6 +53,22 @@ DbgCtl dbg_ctl_log_resolve{"log-resolve"};
 DbgCtl dbg_ctl_log_unmarshal_orun{"log-unmarshal-orun"}; // Overrun of 
unmarshaling destination buffer.
 DbgCtl dbg_ctl_log_unmarshal_data{"log-unmarshal-data"}; // Error in txn data 
when unmarshalling.
 
+int
+marshal_http_version_string(char *buf, HTTPHdr *response)
+{
+  HTTPVersion version = response ? response->version_get() : HTTP_INVALID;
+  char        str[8];
+  int         str_len =
+    snprintf(str, sizeof(str), "%u.%u", 
static_cast<unsigned>(version.get_major()), 
static_cast<unsigned>(version.get_minor()));
+  int len = LogAccess::padded_length(str_len + 1);
+
+  ink_release_assert(str_len > 0 && str_len < static_cast<int>(sizeof(str)));
+  if (buf) {
+    LogAccess::marshal_str(buf, str, len);
+  }
+  return len;
+}
+
 } // end anonymous namespace
 
 #define DBG_UNMARSHAL_DEST_OVERRUN Dbg(dbg_ctl_log_unmarshal_orun, "Unmarshal 
destination buffer overrun.");
@@ -1066,9 +1082,8 @@ LogAccess::marshal_cache_resp_all_header_fields(char *buf)
 /*-------------------------------------------------------------------------
   LogAccess::unmarshal_http_version
 
-  The http version is marshalled as two consecutive integers, the first for
-  the major number and the second for the minor number.  Retrieve both
-  numbers and return the result as "HTTP/major.minor".
+  The HTTP version is marshalled as a "major.minor" string. Prefix it with
+  "HTTP/" for the established text log representation.
   -------------------------------------------------------------------------*/
 
 int
@@ -1078,34 +1093,17 @@ LogAccess::unmarshal_http_version(char **buf, char 
*dest, int len)
   ink_assert(*buf != nullptr);
   ink_assert(dest != nullptr);
 
-  static const char http[]   = "HTTP/";
-  static int        http_len = static_cast<int>(sizeof(http) - 1);
+  static constexpr std::string_view prefix{"HTTP/"};
 
-  char  val_buf[128];
-  char *p = val_buf;
+  char *version     = *buf;
+  int   version_len = static_cast<int>(strlen(version));
+  int   value_len   = static_cast<int>(prefix.size()) + version_len;
 
-  auto vb_left = [&]() -> int { return sizeof(val_buf) - (p - val_buf); };
-
-  memcpy(p, http, http_len);
-  p += http_len;
-
-  int res1 = unmarshal_int_to_str(buf, p, vb_left());
-  if (res1 < 0) {
-    return -1;
-  }
-  p        += res1;
-  *p++      = '.';
-  int res2  = unmarshal_int_to_str(buf, p, vb_left());
-  if (res2 < 0) {
-    DBG_UNMARSHAL_DEST_OVERRUN
-    return -1;
-  }
-  p += res2;
-
-  int val_len = p - val_buf;
-  if (val_len < len) {
-    memcpy(dest, val_buf, val_len);
-    return val_len;
+  *buf += padded_strlen(version);
+  if (value_len <= len) {
+    memcpy(dest, prefix.data(), prefix.size());
+    memcpy(dest + prefix.size(), version, version_len);
+    return value_len;
   }
   DBG_UNMARSHAL_DEST_OVERRUN
   return -1;
@@ -2922,17 +2920,7 @@ LogAccess::marshal_server_resp_squid_len(char *buf)
 int
 LogAccess::marshal_server_resp_http_version(char *buf)
 {
-  if (buf) {
-    int64_t major = 0;
-    int64_t minor = 0;
-    if (m_server_response) {
-      major = m_server_response->version_get().get_major();
-      minor = m_server_response->version_get().get_minor();
-    }
-    marshal_int(buf, major);
-    marshal_int((buf + INK_MIN_ALIGN), minor);
-  }
-  return (2 * INK_MIN_ALIGN);
+  return marshal_http_version_string(buf, m_server_response);
 }
 
 /*-------------------------------------------------------------------------
@@ -3073,17 +3061,7 @@ LogAccess::marshal_cache_resp_header_len(char *buf)
 int
 LogAccess::marshal_cache_resp_http_version(char *buf)
 {
-  if (buf) {
-    int64_t major = 0;
-    int64_t minor = 0;
-    if (m_cache_response) {
-      major = m_cache_response->version_get().get_major();
-      minor = m_cache_response->version_get().get_minor();
-    }
-    marshal_int(buf, major);
-    marshal_int((buf + INK_MIN_ALIGN), minor);
-  }
-  return (2 * INK_MIN_ALIGN);
+  return marshal_http_version_string(buf, m_cache_response);
 }
 
 int
diff --git a/src/proxy/logging/unit-tests/test_LogAccess.cc 
b/src/proxy/logging/unit-tests/test_LogAccess.cc
index a3158aba4d..0a33a6d8df 100644
--- a/src/proxy/logging/unit-tests/test_LogAccess.cc
+++ b/src/proxy/logging/unit-tests/test_LogAccess.cc
@@ -215,23 +215,31 @@ TEST_CASE("LogAccess non-HttpSM client host port is 
null-safe", "[LogAccess]")
   CHECK(marshal_int_value([&](char *buf) { return 
access.marshal_client_host_port(buf); }) == 4321);
 }
 
-TEST_CASE("LogAccess unmarshal_http_version keeps the minor version", 
"[LogAccess]")
+TEST_CASE("LogAccess marshals response HTTP versions as compact strings", 
"[LogAccess]")
 {
-  auto render = [](int64_t major, int64_t minor) -> std::string {
-    char marshalled[2 * INK_MIN_ALIGN];
-    LogAccess::marshal_int(marshalled, major);
-    LogAccess::marshal_int(marshalled + INK_MIN_ALIGN, minor);
+  NonHttpSmLogData data;
+  populate_non_http_sm_data(data, "GET", "https"sv, "example.com", 
"/version"sv);
+  TransactionLogData log_data(data);
+  LogAccess          access(log_data);
+
+  access.init();
+
+  auto check = [](auto marshal) {
+    CHECK(marshal(nullptr) == INK_MIN_ALIGN);
+    CHECK(marshal_string(marshal) == "0.0");
+
+    char marshalled[INK_MIN_ALIGN] = {};
+    marshal(marshalled);
 
     char  dest[64] = {};
     char *src      = marshalled;
     int   len      = LogAccess::unmarshal_http_version(&src, dest, 
sizeof(dest));
+
     REQUIRE(len > 0);
-    return std::string(dest, len);
+    CHECK(std::string(dest, len) == "HTTP/0.0");
+    CHECK(src == marshalled + INK_MIN_ALIGN);
   };
 
-  CHECK(render(1, 1) == "HTTP/1.1");
-  CHECK(render(1, 0) == "HTTP/1.0");
-  // known bug of `.0` suffix for HTTP/2 and HTTP/3
-  CHECK(render(2, 0) == "HTTP/2.0");
-  CHECK(render(3, 0) == "HTTP/3.0");
+  check([&](char *buf) { return access.marshal_server_resp_http_version(buf); 
});
+  check([&](char *buf) { return access.marshal_cache_resp_http_version(buf); 
});
 }
diff --git a/src/proxy/logging/unit-tests/test_LogBuffer.cc 
b/src/proxy/logging/unit-tests/test_LogBuffer.cc
index 4c019e7890..79778e9f5d 100644
--- a/src/proxy/logging/unit-tests/test_LogBuffer.cc
+++ b/src/proxy/logging/unit-tests/test_LogBuffer.cc
@@ -46,9 +46,9 @@ TEST_CASE("LogField::Type reflects each field's on-wire 
framing", "[logging][v3]
   Log::init_fields();
 
   // Each field's declared type must match its on-wire framing, since the 
schema
-  // serializes Type directly: pssc/crc single ints, cqu string, chi IP, sshv a
-  // pair (HTTP version), ppv a string (after the sINT/dINT type fixes).
-  LogFormat fmt("v3wt", "%<pssc> %<crc> %<cqu> %<chi> %<sshv> %<ppv>");
+  // serializes Type directly: pssc/crc single ints, cqu/sshv/csshv/ppv
+  // strings, and chi an IP.
+  LogFormat fmt("v3wt", "%<pssc> %<crc> %<cqu> %<chi> %<sshv> %<csshv> 
%<ppv>");
   REQUIRE(fmt.valid());
 
   std::vector<LogField::Type> types;
@@ -56,8 +56,9 @@ TEST_CASE("LogField::Type reflects each field's on-wire 
framing", "[logging][v3]
     types.push_back(f->type());
   }
 
-  REQUIRE(types == (std::vector<LogField::Type>{LogField::Type::sINT, 
LogField::Type::sINT, LogField::Type::STRING,
-                                                LogField::Type::IP, 
LogField::Type::dINT, LogField::Type::STRING}));
+  REQUIRE(types ==
+          (std::vector<LogField::Type>{LogField::Type::sINT, 
LogField::Type::sINT, LogField::Type::STRING, LogField::Type::IP,
+                                       LogField::Type::STRING, 
LogField::Type::STRING, LogField::Type::STRING}));
 }
 
 TEST_CASE("marshal_int / unmarshal_int round-trip", "[logging][v3]")
diff --git a/src/traffic_logcat/unit-tests/test_LogEntryJson.cc 
b/src/traffic_logcat/unit-tests/test_LogEntryJson.cc
index 6d44506a93..c3ef245ca9 100644
--- a/src/traffic_logcat/unit-tests/test_LogEntryJson.cc
+++ b/src/traffic_logcat/unit-tests/test_LogEntryJson.cc
@@ -295,7 +295,7 @@ TEST_CASE("v3 generic decode rejects field_count not 
matching the symbol list",
 TEST_CASE("v3 generic decode reads a dINT field (16 bytes)", "[logcat][v3]")
 {
   V3Segment seg;
-  init_segment(seg, "sshv", {LogField::Type::dINT});
+  init_segment(seg, "pair", {LogField::Type::dINT});
 
   auto *entry = reinterpret_cast<LogEntryHeader *>(seg.storage + DATA_OFF);
   char *w     = reinterpret_cast<char *>(entry) + sizeof(LogEntryHeader);
@@ -309,13 +309,13 @@ TEST_CASE("v3 generic decode reads a dINT field (16 
bytes)", "[logcat][v3]")
   char out[256];
   int  n = log_entry_to_json(entry, seg.header(), out, sizeof(out));
   REQUIRE(n > 0);
-  CHECK(std::string(out, n) == R"({"sshv":[1,1]})");
+  CHECK(std::string(out, n) == R"({"pair":[1,1]})");
 }
 
 TEST_CASE("v3 generic decode rejects a truncated dINT field", "[logcat][v3]")
 {
   V3Segment seg;
-  init_segment(seg, "sshv", {LogField::Type::dINT});
+  init_segment(seg, "pair", {LogField::Type::dINT});
 
   auto *entry = reinterpret_cast<LogEntryHeader *>(seg.storage + DATA_OFF);
   char *w     = reinterpret_cast<char *>(entry) + sizeof(LogEntryHeader);
diff --git a/tests/gold_tests/logging/binary_log_v3.test.py 
b/tests/gold_tests/logging/binary_log_v3.test.py
index 9701a389f6..e3fad87462 100644
--- a/tests/gold_tests/logging/binary_log_v3.test.py
+++ b/tests/gold_tests/logging/binary_log_v3.test.py
@@ -43,7 +43,7 @@ class BinaryLogV3Test:
     #   chi  = client IP         -> IP     -> "127.0.0.1"
     #   cqu  = request URL       -> STRING -> "http://127.0.0.1:<port>/get"
     #   pssc = response status   -> sINT   -> "200"
-    #   sshv = resp HTTP version -> dINT   -> "HTTP/1.1" (ASCII) / [1,1] (JSON)
+    #   sshv = resp HTTP version -> STRING -> "HTTP/1.1" (ASCII) / "1.1" (JSON)
     # The dynamic listen port in cqu is masked with `` in the gold files.
     log_format = '%<chi> %<cqu> %<pssc> %<sshv>'
 
@@ -151,7 +151,7 @@ class BinaryLogV3Test:
         stdout += Testers.ContainsExpression(r'chi\s+IP', 'chi is framed as an 
IP.')
         stdout += Testers.ContainsExpression(r'cqu\s+STRING', 'cqu is framed 
as a STRING.')
         stdout += Testers.ContainsExpression(r'pssc\s+sINT', 'pssc is framed 
as an sINT.')
-        stdout += Testers.ContainsExpression(r'sshv\s+dINT', 'sshv is framed 
as a dINT.')
+        stdout += Testers.ContainsExpression(r'sshv\s+STRING', 'sshv is framed 
as a STRING.')
 
         # traffic_logcat -H also prints the v2 header, but v2 segments carry no
         # field-type schema.
diff --git a/tests/gold_tests/logging/gold/binary_log_v3_json.gold 
b/tests/gold_tests/logging/gold/binary_log_v3_json.gold
index 3889f46817..452579eb8e 100644
--- a/tests/gold_tests/logging/gold/binary_log_v3_json.gold
+++ b/tests/gold_tests/logging/gold/binary_log_v3_json.gold
@@ -1,3 +1,3 @@
-{"chi":"127.0.0.1","cqu":"http://127.0.0.1:``/get","pssc":200,"sshv":[1,1]}
-{"chi":"127.0.0.1","cqu":"http://127.0.0.1:``/get","pssc":200,"sshv":[1,1]}
-{"chi":"127.0.0.1","cqu":"http://127.0.0.1:``/get","pssc":200,"sshv":[1,1]}
+{"chi":"127.0.0.1","cqu":"http://127.0.0.1:``/get","pssc":200,"sshv":"1.1"}
+{"chi":"127.0.0.1","cqu":"http://127.0.0.1:``/get","pssc":200,"sshv":"1.1"}
+{"chi":"127.0.0.1","cqu":"http://127.0.0.1:``/get","pssc":200,"sshv":"1.1"}

Reply via email to