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 # ======================================================================
