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

zwoop 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 0dc18dd902 Add cache key hash logging field and public API (#13134)
0dc18dd902 is described below

commit 0dc18dd9028f889803f944e157942783ddbb769d
Author: Leif Hedstrom <[email protected]>
AuthorDate: Mon May 18 16:47:58 2026 -0600

    Add cache key hash logging field and public API (#13134)
    
    * Add cache key hash logging field and public API
    
    Add 'ckh' log field that emits the base64-encoded cache key
    digest, and TSHttpTxnCacheKeyDigestGet() for plugin access to
    the raw hash bytes. Includes admin and API documentation.
    
    Co-Author: Craig Taylor
    
    * Address Copilot and bneradt review comments
    
    Set *length on all return paths in TSHttpTxnCacheKeyDigestGet(),
    and use base64.b64decode(validate=True) for stricter validation
    in the test verifier. Also switch the @param tags on
    TSHttpTxnCacheKeyDigestGet() to use [in]/[out]/[in,out] per
    AGENTS.md.
---
 doc/admin-guide/logging/formatting.en.rst          |  5 ++
 .../functions/TSHttpTxnCacheKeyDigestGet.en.rst    | 64 ++++++++++++++++++++++
 include/proxy/http/HttpCacheSM.h                   |  6 ++
 include/proxy/logging/LogAccess.h                  |  1 +
 include/proxy/logging/TransactionLogData.h         |  5 +-
 include/ts/ts.h                                    | 19 +++++++
 src/api/InkAPI.cc                                  | 29 ++++++++++
 src/proxy/logging/Log.cc                           |  4 ++
 src/proxy/logging/LogAccess.cc                     | 30 ++++++++++
 src/proxy/logging/TransactionLogData.cc            | 10 ++++
 .../logging/log-milestone-fields.test.py           |  4 +-
 .../gold_tests/logging/verify_milestone_fields.py  | 28 +++++++++-
 12 files changed, 199 insertions(+), 6 deletions(-)

diff --git a/doc/admin-guide/logging/formatting.en.rst 
b/doc/admin-guide/logging/formatting.en.rst
index 48ce9402a8..0a95f383d7 100644
--- a/doc/admin-guide/logging/formatting.en.rst
+++ b/doc/admin-guide/logging/formatting.en.rst
@@ -151,6 +151,7 @@ Cache Details
 .. _crc:
 .. _crsc:
 .. _chm:
+.. _ckh:
 .. _cwr:
 .. _cwtr:
 .. _crra:
@@ -166,6 +167,10 @@ Field Source         Description
 cluc  Client Request Cache Lookup URL, also known as the :term:`cache key`,
                      which is the canonicalized version of the client request
                      URL.
+ckh   Proxy Cache    Cache Key Hash. The base64-encoded cryptographic hash of 
the
+                     effective cache key used for cache lookup and storage. 
This
+                     is the actual key used to index cache objects. Empty
+                     (``-``) when no cache lookup was performed.
 crc   Proxy Cache    Cache Result Code. The result of |TS| attempting to obtain
                      the object from cache; :ref:`admin-logging-cache-results`.
 crsc  Proxy Cache    Cache Result Sub-Code. More specific code to complement 
the
diff --git 
a/doc/developer-guide/api/functions/TSHttpTxnCacheKeyDigestGet.en.rst 
b/doc/developer-guide/api/functions/TSHttpTxnCacheKeyDigestGet.en.rst
new file mode 100644
index 0000000000..28d8ea3fc9
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSHttpTxnCacheKeyDigestGet.en.rst
@@ -0,0 +1,64 @@
+.. 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:: ../../../common.defs
+
+.. default-domain:: cpp
+
+TSHttpTxnCacheKeyDigestGet
+**************************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/ts.h>
+
+.. function:: TSReturnCode TSHttpTxnCacheKeyDigestGet(TSHttpTxn txnp, char 
*buffer, int *length)
+
+Description
+===========
+
+Get the effective cache key digest (cryptographic hash) that was used for
+cache lookup or storage on this transaction. This is the raw hash bytes,
+not a hex or base64 encoding.
+
+The digest size depends on the build configuration: 16 bytes for MD5
+(default) or 32 bytes for SHA-256 (FIPS mode). A 32-byte buffer is
+sufficient for either mode:
+
+.. code-block:: c
+
+    char digest[32];
+    int  digest_len = sizeof(digest);
+    if (TSHttpTxnCacheKeyDigestGet(txnp, digest, &digest_len) == TS_SUCCESS) {
+      // digest_len contains the actual number of bytes written
+    }
+
+Pass :code:`nullptr` for *buffer* to query the digest size without
+copying.
+
+Returns :enumerator:`TS_SUCCESS` if a cache key was computed for the
+transaction. Returns :enumerator:`TS_ERROR` if no cache lookup was
+performed, or if *buffer* is non-null and *\*length* is smaller than the
+digest size. In all cases *\*length* is set to the required digest size
+on return.
+
+See Also
+========
+
+:func:`TSHttpTxnCacheLookupUrlGet`
diff --git a/include/proxy/http/HttpCacheSM.h b/include/proxy/http/HttpCacheSM.h
index 41623103d6..b1379e0555 100644
--- a/include/proxy/http/HttpCacheSM.h
+++ b/include/proxy/http/HttpCacheSM.h
@@ -129,6 +129,12 @@ public:
     return cache_read_vc ? (cache_read_vc->is_compressed_in_ram()) : false;
   }
 
+  const HttpCacheKey &
+  get_cache_key() const
+  {
+    return cache_key;
+  }
+
   void
   set_open_read_tries(int value)
   {
diff --git a/include/proxy/logging/LogAccess.h 
b/include/proxy/logging/LogAccess.h
index e449b674b2..51d80471f8 100644
--- a/include/proxy/logging/LogAccess.h
+++ b/include/proxy/logging/LogAccess.h
@@ -254,6 +254,7 @@ public:
   //
   int marshal_cache_write_code(char *);           // INT
   int marshal_cache_write_transform_code(char *); // INT
+  int marshal_cache_key_hash(char *);             // STR
 
   // other fields
   //
diff --git a/include/proxy/logging/TransactionLogData.h 
b/include/proxy/logging/TransactionLogData.h
index 0c1eefd53a..0db209e5a2 100644
--- a/include/proxy/logging/TransactionLogData.h
+++ b/include/proxy/logging/TransactionLogData.h
@@ -75,8 +75,9 @@ public:
   int   get_unmapped_url_len() const;
 
   // ===== Cache lookup URL =====
-  char *get_cache_lookup_url_str() const;
-  int   get_cache_lookup_url_len() const;
+  char                 *get_cache_lookup_url_str() const;
+  int                   get_cache_lookup_url_len() const;
+  const ts::CryptoHash *get_cache_lookup_hash() const;
 
   // ===== Client addressing =====
   sockaddr const *get_client_addr() const;
diff --git a/include/ts/ts.h b/include/ts/ts.h
index 9b62d64fe8..ad266054cb 100644
--- a/include/ts/ts.h
+++ b/include/ts/ts.h
@@ -2768,6 +2768,25 @@ TSReturnCode TSHttpTxnCachedRespModifiableGet(TSHttpTxn 
txnp, TSMBuffer *bufp, T
 TSReturnCode TSHttpTxnCacheLookupStatusSet(TSHttpTxn txnp, int cachelookup);
 TSReturnCode TSHttpTxnCacheLookupUrlGet(TSHttpTxn txnp, TSMBuffer bufp, TSMLoc 
obj);
 TSReturnCode TSHttpTxnCacheLookupUrlSet(TSHttpTxn txnp, TSMBuffer bufp, TSMLoc 
obj);
+
+/**
+    Gets the effective cache key digest (cryptographic hash) that was
+    used for cache lookup or storage on this transaction.  The digest
+    is returned as raw bytes — 16 bytes for MD5 (default) or 32 bytes
+    for SHA-256 (FIPS mode).  A buffer of at least 32 bytes is
+    recommended to accommodate either configuration.
+
+    @param[in] txnp the transaction.
+    @param[out] buffer caller-provided buffer to receive the raw hash
+      bytes. If @c nullptr, only @a length is set (size query).
+    @param[in,out] length capacity of @a buffer in bytes on input; actual
+      digest size in bytes on output.
+
+    @return @c TS_SUCCESS if a cache key was computed for this
+      transaction, @c TS_ERROR if no cache lookup was performed or if
+      @a buffer is non-null and too small.
+ */
+TSReturnCode TSHttpTxnCacheKeyDigestGet(TSHttpTxn txnp, char *buffer, int 
*length);
 TSReturnCode TSHttpTxnPrivateSessionSet(TSHttpTxn txnp, int private_session);
 const char  *TSHttpTxnCacheDiskPathGet(TSHttpTxn txnp, int *length);
 int          TSHttpTxnBackgroundFillStarted(TSHttpTxn txnp);
diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc
index b5e1c775d5..c5f80b6ef1 100644
--- a/src/api/InkAPI.cc
+++ b/src/api/InkAPI.cc
@@ -4477,6 +4477,35 @@ TSHttpTxnCacheLookupUrlSet(TSHttpTxn txnp, TSMBuffer 
bufp, TSMLoc obj)
   return TS_SUCCESS;
 }
 
+TSReturnCode
+TSHttpTxnCacheKeyDigestGet(TSHttpTxn txnp, char *buffer, int *length)
+{
+  sdk_assert(sdk_sanity_check_txn(txnp) == TS_SUCCESS);
+  sdk_assert(length != nullptr);
+
+  HttpSM           *sm              = reinterpret_cast<HttpSM *>(txnp);
+  const CryptoHash &hash            = sm->get_cache_sm().get_cache_key().hash;
+  constexpr int     size            = CRYPTO_HASH_SIZE;
+  int               provided_length = *length;
+
+  *length = size;
+
+  if (hash.is_zero()) {
+    return TS_ERROR;
+  }
+
+  if (buffer == nullptr) {
+    return TS_SUCCESS;
+  }
+
+  if (provided_length < size) {
+    return TS_ERROR;
+  }
+
+  memcpy(buffer, hash.u8, size);
+  return TS_SUCCESS;
+}
+
 /**
  * timeout is in msec
  * overrides as proxy.config.http.transaction_active_timeout_out
diff --git a/src/proxy/logging/Log.cc b/src/proxy/logging/Log.cc
index 49a21978b7..9a0ebd1eb1 100644
--- a/src/proxy/logging/Log.cc
+++ b/src/proxy/logging/Log.cc
@@ -505,6 +505,10 @@ Log::init_fields()
   global_field_list.add(field, false);
   field_symbol_hash.emplace("cluc", field);
 
+  field = new LogField("cache_key_hash", "ckh", LogField::STRING, 
&LogAccess::marshal_cache_key_hash, &LogAccess::unmarshal_str);
+  global_field_list.add(field, false);
+  field_symbol_hash.emplace("ckh", field);
+
   field = new LogField("client_sni_server_name", "cssn", LogField::STRING, 
&LogAccess::marshal_client_sni_server_name,
                        &LogAccess::unmarshal_str);
   global_field_list.add(field, false);
diff --git a/src/proxy/logging/LogAccess.cc b/src/proxy/logging/LogAccess.cc
index 91b8135561..2bb9ed83cf 100644
--- a/src/proxy/logging/LogAccess.cc
+++ b/src/proxy/logging/LogAccess.cc
@@ -35,6 +35,7 @@
 #include "swoc/BufferWriter.h"
 #include "tscore/Encoding.h"
 #include "tscore/ink_inet.h"
+#include "tscore/ink_base64.h"
 
 char INVALID_STR[] = "!INVALID_STR!";
 
@@ -3030,6 +3031,35 @@ LogAccess::marshal_cache_write_transform_code(char *buf)
   return INK_MIN_ALIGN;
 }
 
+/*-------------------------------------------------------------------------
+  -------------------------------------------------------------------------*/
+
+int
+LogAccess::marshal_cache_key_hash(char *buf)
+{
+  const ts::CryptoHash *hash = m_data->get_cache_lookup_hash();
+
+  if (!hash || hash->is_zero()) {
+    if (buf) {
+      marshal_str(buf, "-", padded_length(2));
+    }
+    return padded_length(2);
+  }
+
+  constexpr size_t b64_bufsize = ats_base64_encode_dstlen(CRYPTO_HASH_SIZE);
+  char             b64_str[b64_bufsize];
+  size_t           b64_len = 0;
+
+  ats_base64_encode(reinterpret_cast<const char *>(hash->u8), 
CRYPTO_HASH_SIZE, b64_str, b64_bufsize, &b64_len);
+
+  int len = padded_length(b64_len + 1);
+
+  if (buf) {
+    marshal_str(buf, b64_str, len);
+  }
+  return len;
+}
+
 /*-------------------------------------------------------------------------
   -------------------------------------------------------------------------*/
 
diff --git a/src/proxy/logging/TransactionLogData.cc 
b/src/proxy/logging/TransactionLogData.cc
index af44b5e374..b1b50e3730 100644
--- a/src/proxy/logging/TransactionLogData.cc
+++ b/src/proxy/logging/TransactionLogData.cc
@@ -366,6 +366,16 @@ TransactionLogData::get_cache_lookup_url_len() const
   return 0;
 }
 
+const ts::CryptoHash *
+TransactionLogData::get_cache_lookup_hash() const
+{
+  if (likely(m_http_sm != nullptr)) {
+    return &(m_http_sm->get_cache_sm().get_cache_key().hash);
+  }
+
+  return nullptr;
+}
+
 // ===== Client addressing =====
 
 sockaddr const *
diff --git a/tests/gold_tests/logging/log-milestone-fields.test.py 
b/tests/gold_tests/logging/log-milestone-fields.test.py
index e2a47ccb34..72f5f8617b 100644
--- a/tests/gold_tests/logging/log-milestone-fields.test.py
+++ b/tests/gold_tests/logging/log-milestone-fields.test.py
@@ -39,9 +39,9 @@ class MilestoneFieldsTest:
     - No epoch-length garbage values (> 1_000_000_000)
     """
 
-    # All Phase 1 msdms fields plus ms and cache result code for 
identification.
+    # All Phase 1 msdms fields plus ms, cache result code, and cache key hash.
     LOG_FORMAT = (
-        'crc=%<crc> ms=%<ttms>'
+        'crc=%<crc> ckh=%<ckh> ms=%<ttms>'
         ' c_ttfb=%<{TS_MILESTONE_UA_BEGIN_WRITE-TS_MILESTONE_SM_START}msdms>'
         ' 
c_tls=%<{TS_MILESTONE_TLS_HANDSHAKE_END-TS_MILESTONE_TLS_HANDSHAKE_START}msdms>'
         ' 
c_hdr=%<{TS_MILESTONE_UA_READ_HEADER_DONE-TS_MILESTONE_SM_START}msdms>'
diff --git a/tests/gold_tests/logging/verify_milestone_fields.py 
b/tests/gold_tests/logging/verify_milestone_fields.py
index 419b8ccc9a..01e91ac00a 100644
--- a/tests/gold_tests/logging/verify_milestone_fields.py
+++ b/tests/gold_tests/logging/verify_milestone_fields.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 '''
-Validate milestone timing fields in an ATS log file.
+Validate milestone timing fields and cache key hash in an ATS log file.
 
 Parses key=value log lines and checks:
   - All expected fields are present
@@ -9,6 +9,8 @@ Parses key=value log lines and checks:
   - Cache miss lines have ms > 0 and origin-phase fields populated
   - Cache hit lines have hit_proc and hit_xfer populated
   - The miss-path chain sums to approximately c_ttfb
+  - Cache key hash (ckh) is a valid base64 string on every line
+  - Cache key hash is identical between miss and hit for the same URL
 '''
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
@@ -26,10 +28,12 @@ Parses key=value log lines and checks:
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+import base64
 import sys
 
 ALL_FIELDS = [
     'crc',
+    'ckh',
     'ms',
     'c_ttfb',
     'c_tls',
@@ -47,7 +51,7 @@ ALL_FIELDS = [
     'hit_xfer',
 ]
 
-TIMING_FIELDS = [f for f in ALL_FIELDS if f != 'crc']
+TIMING_FIELDS = [f for f in ALL_FIELDS if f not in ('crc', 'ckh')]
 
 # Fields that form the contiguous miss-path chain to c_ttfb:
 #   c_ttfb = c_hdr + c_proc + cache + dns + o_conn + o_wait + o_hdr + o_proc
@@ -77,6 +81,18 @@ def validate_line(fields: dict[str, str], line_num: int) -> 
list[str]:
         if name not in fields:
             errors.append(f'line {line_num}: missing field "{name}"')
 
+    ckh = fields.get('ckh')
+    if ckh is not None:
+        if ckh == '-':
+            errors.append(f'line {line_num}: ckh should not be "-" (cache 
lookup was performed)')
+        else:
+            try:
+                raw = base64.b64decode(ckh, validate=True)
+                if len(raw) not in (16, 32):
+                    errors.append(f'line {line_num}: ckh decoded to {len(raw)} 
bytes (expected 16 or 32)')
+            except Exception as e:
+                errors.append(f'line {line_num}: ckh is not valid base64: 
{ckh!r} ({e})')
+
     for name in TIMING_FIELDS:
         val_str = fields.get(name)
         if val_str is None:
@@ -183,6 +199,7 @@ def main():
     all_errors = []
     miss_found = False
     hit_found = False
+    cache_key_hashes = set()
 
     for i, line in enumerate(lines, start=1):
         fields = parse_line(line)
@@ -191,6 +208,9 @@ def main():
             miss_found = True
         if 'HIT' in crc and 'MISS' not in crc:
             hit_found = True
+        ckh = fields.get('ckh')
+        if ckh and ckh != '-':
+            cache_key_hashes.add(ckh)
         errors = validate_line(fields, i)
         all_errors.extend(errors)
 
@@ -198,6 +218,10 @@ def main():
         all_errors.append('No cache miss line found in log')
     if not hit_found:
         all_errors.append('No cache hit line found in log')
+    if len(cache_key_hashes) != 1:
+        all_errors.append(
+            f'Expected identical cache key hash on all lines, got 
{len(cache_key_hashes)} '
+            f'distinct values: {cache_key_hashes}')
 
     if all_errors:
         for err in all_errors:

Reply via email to