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

commit 7a83524405ee1b3d6cdfec8a8ab4e2ebd28cd04e
Author: Masakazu Kitajo <[email protected]>
AuthorDate: Fri Apr 10 16:59:55 2026 -0600

    jax_fingerprint: Add --log-field option for custom log fields (#13084)
    
    - Add --log-field <symbol> option to jax_fingerprint plugin that registers 
a custom log field usable in logging.yaml format strings (e.g., --log-field 
jaxja4 enables %<jaxja4>)
    - Change TSLogMarshalCallback and LogField::CustomMarshalFunc from raw 
function pointers tostd::function to support capturing lambdas, enabling 
plugins to register log field callbacks with state
    
    (cherry picked from commit f37bcf92fbb2f1ccc46a04028dadd12dc8df69fd)
---
 doc/admin-guide/plugins/jax_fingerprint.en.rst     | 11 ++++++
 include/proxy/logging/LogAccess.h                  |  2 +-
 include/proxy/logging/LogField.h                   | 40 ++++++++++-----------
 include/ts/apidefs.h.in                            |  3 +-
 plugins/experimental/jax_fingerprint/config.h      |  1 +
 plugins/experimental/jax_fingerprint/plugin.cc     | 32 +++++++++++++++++
 src/api/InkAPI.cc                                  |  5 +--
 src/proxy/logging/LogAccess.cc                     |  2 +-
 src/proxy/logging/LogField.cc                      |  4 +--
 .../jax_fingerprint/jax_fingerprint.test.py        | 41 +++++++++++++++++++++-
 10 files changed, 113 insertions(+), 28 deletions(-)

diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst 
b/doc/admin-guide/plugins/jax_fingerprint.en.rst
index e500cba6a2..54b9e04afb 100644
--- a/doc/admin-guide/plugins/jax_fingerprint.en.rst
+++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst
@@ -85,6 +85,17 @@ This option specifies the name of the header field where the 
plugin stores the g
 
 This option specifies the filename for the plugin log file. If not specified, 
log output will be suppressed.
 
+.. option:: --log-field <symbol>
+
+This option registers a custom log field with the given symbol name that can 
be used in
+:file:`logging.yaml` log formats. The log field outputs the generated 
fingerprint value for each
+transaction. If not specified, no custom log field is registered.
+
+For example, if you specify ``--log-field jaxja4``, you can use ``%<jaxja4>`` 
in your log format
+string in :file:`logging.yaml`.
+
+.. note:: This option is only supported when the plugin is loaded as a global 
plugin in :file:`plugin.config`. Log fields are global and must be registered 
before log formats are parsed at startup. If you use a remap-only setup, you 
must also load the plugin globally with ``--log-field`` to register the log 
field.
+
 
 Plugin Behavior
 ===============
diff --git a/include/proxy/logging/LogAccess.h 
b/include/proxy/logging/LogAccess.h
index d56078ac68..31e94d7e4d 100644
--- a/include/proxy/logging/LogAccess.h
+++ b/include/proxy/logging/LogAccess.h
@@ -325,7 +325,7 @@ public:
   void set_http_header_field(LogField::Container container, char *field, char 
*buf, int len);
 
   // Plugin
-  int marshal_custom_field(char *buf, LogField::CustomMarshalFunc 
plugin_marshal_func);
+  int marshal_custom_field(char *buf, const LogField::CustomMarshalFunc 
&plugin_marshal_func);
 
   //
   // unmarshalling routines
diff --git a/include/proxy/logging/LogField.h b/include/proxy/logging/LogField.h
index 7fbaca429e..27a1fb6b20 100644
--- a/include/proxy/logging/LogField.h
+++ b/include/proxy/logging/LogField.h
@@ -23,6 +23,7 @@
 
 #pragma once
 
+#include <functional>
 #include <memory>
 #include <optional>
 #include <string_view>
@@ -88,7 +89,7 @@ public:
   using UnmarshalFuncWithSlice = int (*)(char **, char *, int, LogSlice *, 
LogEscapeType);
   using UnmarshalFuncWithMap   = int (*)(char **, char *, int, const 
Ptr<LogFieldAliasMap> &);
   using SetFunc                = void (LogAccess::*)(char *, int);
-  using CustomMarshalFunc      = int (*)(void *, char *);
+  using CustomMarshalFunc      = std::function<int(void *, char *)>;
   using CustomUnmarshalFunc    = std::tuple<int, int> (*)(char **, char *, 
int);
 
   using VarUnmarshalFuncSliceOnly = std::variant<UnmarshalFunc, 
UnmarshalFuncWithSlice>;
@@ -208,25 +209,24 @@ public:
   static bool      isContainerUpdateFieldSupported(Container container);
 
 private:
-  char                 *m_name;
-  char                 *m_symbol;
-  Type                  m_type;
-  Container             m_container;
-  MarshalFunc           m_marshal_func;   // place data into buffer
-  VarUnmarshalFunc      m_unmarshal_func; // create a string of the data
-  Aggregate             m_agg_op;
-  int64_t               m_agg_cnt;
-  int64_t               m_agg_val;
-  TSMilestonesType      m_milestone1; ///< Used for MS and MSDMS as the first 
(or only) milestone.
-  TSMilestonesType      m_milestone2; ///< Second milestone for MSDMS
-  bool                  m_time_field;
-  Ptr<LogFieldAliasMap> m_alias_map; // map sINT <--> string
-  SetFunc               m_set_func;
-  TSMilestonesType      milestone_from_m_name();
-  int                   milestones_from_m_name(TSMilestonesType *m1, 
TSMilestonesType *m2);
-  CustomMarshalFunc     m_custom_marshal_func   = nullptr;
-  CustomUnmarshalFunc   m_custom_unmarshal_func = nullptr;
-
+  char                      *m_name;
+  char                      *m_symbol;
+  Type                       m_type;
+  Container                  m_container;
+  MarshalFunc                m_marshal_func;   // place data into buffer
+  VarUnmarshalFunc           m_unmarshal_func; // create a string of the data
+  Aggregate                  m_agg_op;
+  int64_t                    m_agg_cnt;
+  int64_t                    m_agg_val;
+  TSMilestonesType           m_milestone1; ///< Used for MS and MSDMS as the 
first (or only) milestone.
+  TSMilestonesType           m_milestone2; ///< Second milestone for MSDMS
+  bool                       m_time_field;
+  Ptr<LogFieldAliasMap>      m_alias_map; // map sINT <--> string
+  SetFunc                    m_set_func;
+  TSMilestonesType           milestone_from_m_name();
+  int                        milestones_from_m_name(TSMilestonesType *m1, 
TSMilestonesType *m2);
+  CustomMarshalFunc          m_custom_marshal_func;
+  CustomUnmarshalFunc        m_custom_unmarshal_func = nullptr;
   std::vector<HeaderField>   m_fallback_header_fields;
   std::unique_ptr<LogField>  m_fallback_field;
   std::optional<std::string> m_fallback_default;
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index 3641f98a13..bce145c8a4 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -43,6 +43,7 @@
  */
 
 #include <cstdint>
+#include <functional>
 #include <memory>
 #include <vector>
 #include <netinet/in.h>
@@ -1162,7 +1163,7 @@ using TSFetchSM = struct tsapi_fetchsm *;
 using TSThreadFunc           = void *(*)(void *data);
 using TSEventFunc            = int (*)(TSCont contp, TSEvent event, void 
*edata);
 using TSConfigDestroyFunc    = void (*)(void *data);
-using TSLogMarshalCallback   = int (*)(TSHttpTxn, char *);
+using TSLogMarshalCallback   = std::function<int(TSHttpTxn, char *)>;
 using TSLogUnmarshalCallback = std::tuple<int, int> (*)(char **, char *, int);
 
 struct TSFetchEvent {
diff --git a/plugins/experimental/jax_fingerprint/config.h 
b/plugins/experimental/jax_fingerprint/config.h
index 3d49894bff..415ad4961d 100644
--- a/plugins/experimental/jax_fingerprint/config.h
+++ b/plugins/experimental/jax_fingerprint/config.h
@@ -62,6 +62,7 @@ struct PluginConfig {
   std::string   header_name     = "";
   std::string   via_header_name = "";
   std::string   log_filename    = "";
+  std::string   log_symbol      = "";
   int           user_arg_index  = -1;
   TSCont        handler         = nullptr; // For remap plugin
   bool          standalone      = false;
diff --git a/plugins/experimental/jax_fingerprint/plugin.cc 
b/plugins/experimental/jax_fingerprint/plugin.cc
index 5044e43e52..aca0ba4eff 100644
--- a/plugins/experimental/jax_fingerprint/plugin.cc
+++ b/plugins/experimental/jax_fingerprint/plugin.cc
@@ -59,6 +59,7 @@ read_config_option(int argc, char const *argv[], PluginConfig 
&config)
     {"header",       required_argument, nullptr, 'h'},
     {"via-header",   required_argument, nullptr, 'v'},
     {"log-filename", required_argument, nullptr, 'f'},
+    {"log-field",    required_argument, nullptr, 'l'},
     {"servernames",  required_argument, nullptr, 'S'},
     {nullptr,        0,                 nullptr, 0  }
   };
@@ -113,6 +114,9 @@ read_config_option(int argc, char const *argv[], 
PluginConfig &config)
         input.remove_prefix(pos == std::string_view::npos ? input.size() : pos 
+ 1);
       }
       break;
+    case 'l':
+      config.log_symbol = {optarg, strlen(optarg)};
+      break;
     case 0:
     case -1:
       break;
@@ -363,6 +367,28 @@ TSPluginInit(int argc, char const **argv)
     }
   }
 
+  if (!config->log_symbol.empty()) {
+    std::string name  = "jax_fingerprint-";
+    name             += config->method.name;
+    TSLogFieldRegister(
+      name.c_str(), config->log_symbol, TS_LOG_TYPE_STRING,
+      [config](TSHttpTxn txnp, char *buf) -> int {
+        void *container;
+        if (config->method.type == Method::Type::CONNECTION_BASED) {
+          container = TSHttpSsnClientVConnGet(TSHttpTxnSsnGet(txnp));
+        } else {
+          container = txnp;
+        }
+        JAxContext *ctx = get_user_arg(container, *config);
+        if (ctx) {
+          return TSLogStringMarshal(buf, ctx->get_fingerprint());
+        } else {
+          return TSLogStringMarshal(buf, "-");
+        }
+      },
+      TSLogIntUnmarshal);
+  }
+
   if (reserve_user_arg(*config) == TS_ERROR) {
     TSError("[%s] Failed to reserve user arg index.", PLUGIN_NAME);
     return;
@@ -406,6 +432,12 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char 
* /* errbuf ATS_UNUSE
     return TS_ERROR;
   }
 
+  if (!config->log_symbol.empty()) {
+    TSError("[%s] --log-field is not supported in remap.config. Use it in 
plugin.config instead.", PLUGIN_NAME);
+    delete config;
+    return TS_ERROR;
+  }
+
   // Create a log file
   if (!config->log_filename.empty()) {
     if (!create_log_file(config->log_filename, config->log_handle)) {
diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc
index ebb5185792..d5e408d31f 100644
--- a/src/api/InkAPI.cc
+++ b/src/api/InkAPI.cc
@@ -9179,8 +9179,9 @@ TSLogFieldRegister(std::string_view name, 
std::string_view symbol, TSLogType typ
     }
   }
 
-  LogField *field = new LogField(name.data(), symbol.data(), 
static_cast<LogField::Type>(type),
-                                 
reinterpret_cast<LogField::CustomMarshalFunc>(marshal_cb), unmarshal_cb);
+  LogField *field = new LogField(
+    name.data(), symbol.data(), static_cast<LogField::Type>(type),
+    [marshal_cb](void *sm, char *buf) -> int { return 
marshal_cb(reinterpret_cast<TSHttpTxn>(sm), buf); }, unmarshal_cb);
   Log::global_field_list.add(field, false);
   Log::field_symbol_hash.emplace(symbol.data(), field);
 
diff --git a/src/proxy/logging/LogAccess.cc b/src/proxy/logging/LogAccess.cc
index b671da02e4..690e9a8f5d 100644
--- a/src/proxy/logging/LogAccess.cc
+++ b/src/proxy/logging/LogAccess.cc
@@ -465,7 +465,7 @@ LogAccess::marshal_ip(char *dest, sockaddr const *ip)
 }
 
 int
-LogAccess::marshal_custom_field(char *buf, LogField::CustomMarshalFunc 
plugin_marshal_func)
+LogAccess::marshal_custom_field(char *buf, const LogField::CustomMarshalFunc 
&plugin_marshal_func)
 {
   int len = plugin_marshal_func(m_data, buf);
   return LogAccess::padded_length(len);
diff --git a/src/proxy/logging/LogField.cc b/src/proxy/logging/LogField.cc
index 2c8378ef48..3860f98504 100644
--- a/src/proxy/logging/LogField.cc
+++ b/src/proxy/logging/LogField.cc
@@ -521,7 +521,7 @@ LogField::marshal_len(LogAccess *lad)
   }
 
   if (m_container == NO_CONTAINER) {
-    if (m_custom_marshal_func == nullptr) {
+    if (!m_custom_marshal_func) {
       return (lad->*m_marshal_func)(nullptr);
     } else {
       return lad->marshal_custom_field(nullptr, m_custom_marshal_func);
@@ -630,7 +630,7 @@ LogField::marshal(LogAccess *lad, char *buf)
   }
 
   if (m_container == NO_CONTAINER) {
-    if (m_custom_marshal_func == nullptr) {
+    if (!m_custom_marshal_func) {
       return (lad->*m_marshal_func)(buf);
     } else {
       return lad->marshal_custom_field(buf, m_custom_marshal_func);
diff --git 
a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py 
b/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py
index 6de5691f01..5cb8389618 100644
--- a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py
+++ b/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py
@@ -43,7 +43,14 @@ class JaxFingerprintTest:
     _client_counter: int = 0
 
     def __init__(
-            self, name: str, method: str, setup: str, mode: str = 'overwrite', 
http2: bool = False, servernames: str = '') -> None:
+            self,
+            name: str,
+            method: str,
+            setup: str,
+            mode: str = 'overwrite',
+            http2: bool = False,
+            servernames: str = '',
+            log_field: str = '') -> None:
         '''Configure test processes for the jax_fingerprint plugin.
 
         :param name: Descriptive name for this test run.
@@ -60,6 +67,10 @@ class JaxFingerprintTest:
             no context is created, and handle_read_request_hdr is a no-op.
             Only meaningful for CONNECTION_BASED methods (JA3/JA4) in global
             setup.
+        :param log_field: Symbol name for --log-field option.  When set,
+            configures logging.yaml with a custom format using the symbol
+            and verifies the fingerprint appears in the ATS access log.
+            Only supported with global setup.
 
         Method notes:
           - JA3 / JA4 are CONNECTION_BASED (triggered on TLS client hello)
@@ -86,6 +97,7 @@ class JaxFingerprintTest:
         self._mode = mode
         self._http2 = http2
         self._servernames = servernames
+        self._log_field = log_field
         # HTTP/2 always runs over TLS (h2 requires TLS).
         self._needs_tls = method in ('JA3', 'JA4') or http2
         self._replay_file = self._choose_replay_file()
@@ -98,6 +110,12 @@ class JaxFingerprintTest:
         Test.AddAwaitFileContainsTestRun(
             f'Await jax_fingerprint.log for: {self._name}', 
self._ts.Disk.jax_log.AbsPath, self._method)
 
+        if self._log_field:
+            log_field_path = os.path.join(self._ts.Variables.LOGDIR, 
'jax_log_field.log')
+            # Verify the log contains a fingerprint (not just a dash 
placeholder).
+            Test.AddAwaitFileContainsTestRun(
+                f'Await jax_log_field.log for: {self._name}', log_field_path, 
f'{self._method}: [a-z0-9]')
+
     # ------------------------------------------------------------------
     # Helpers
     # ------------------------------------------------------------------
@@ -222,6 +240,8 @@ class JaxFingerprintTest:
                 global_args += f' --mode {self._mode}'
             if self._servernames:
                 global_args += f' --servernames {self._servernames}'
+            if self._log_field:
+                global_args += f' --log-field {self._log_field}'
             self._ts.Disk.plugin_config.AddLine(f'jax_fingerprint.so 
{global_args}')
             self._ts.Disk.remap_config.AddLine(f'map 
{scheme}://jax.server.test {backend}')
             if self._servernames:
@@ -261,6 +281,18 @@ class JaxFingerprintTest:
                 self._ts.Disk.remap_config.AddLine(
                     f'map https://jax.server.test 
https://jax.backend.test:{server_port} {remap_line}')
 
+        if self._log_field:
+            self._ts.Disk.logging_yaml.AddLines(
+                f'''
+logging:
+  formats:
+    - name: jax_custom
+      format: '{self._method}: %<{self._log_field}>'
+  logs:
+    - filename: jax_log_field
+      format: jax_custom
+'''.split("\n"))
+
     def _configure_client(self, tr: 'TestRun') -> None:
         '''Configure the verifier client.'''
         name = f'client{JaxFingerprintTest._client_counter}'
@@ -332,6 +364,13 @@ JaxFingerprintTest('Global JA4 servernames', 'JA4', 
'global', servernames='jax.s
 # connection has a vconn context, so only that request gets headers.
 JaxFingerprintTest('Hybrid JA4 servernames', 'JA4', 'hybrid', 
servernames='jax.server.test')
 
+# --- Custom log field (--log-field) -----------------------------------------
+
+# Register a custom log field via --log-field and verify the fingerprint
+# appears in the ATS access log configured in logging.yaml.
+JaxFingerprintTest('Global JA4H log-field', 'JA4H', 'global', 
log_field='jaxja4h')
+JaxFingerprintTest('Global JA4 log-field', 'JA4', 'global', log_field='jaxja4')
+
 # ======================================================================
 # All Methods Test - Verify shared context map works with multiple methods
 # ======================================================================

Reply via email to