Skip to content

Commit f37bcf9

Browse files
authored
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
1 parent 18112d8 commit f37bcf9

10 files changed

Lines changed: 96 additions & 10 deletions

File tree

doc/admin-guide/plugins/jax_fingerprint.en.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ This option specifies the name of the header field where the plugin stores the g
8585

8686
This option specifies the filename for the plugin log file. If not specified, log output will be suppressed.
8787

88+
.. option:: --log-field <symbol>
89+
90+
This option registers a custom log field with the given symbol name that can be used in
91+
:file:`logging.yaml` log formats. The log field outputs the generated fingerprint value for each
92+
transaction. If not specified, no custom log field is registered.
93+
94+
For example, if you specify ``--log-field jaxja4``, you can use ``%<jaxja4>`` in your log format
95+
string in :file:`logging.yaml`.
96+
97+
.. 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.
98+
8899

89100
Plugin Behavior
90101
===============

include/proxy/logging/LogAccess.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ class LogAccess
310310
void set_http_header_field(LogField::Container container, char *field, char *buf, int len);
311311

312312
// Plugin
313-
int marshal_custom_field(char *buf, LogField::CustomMarshalFunc plugin_marshal_func);
313+
int marshal_custom_field(char *buf, const LogField::CustomMarshalFunc &plugin_marshal_func);
314314

315315
//
316316
// unmarshalling routines

include/proxy/logging/LogField.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#pragma once
2525

26+
#include <functional>
2627
#include <memory>
2728
#include <optional>
2829
#include <string_view>
@@ -88,7 +89,7 @@ class LogField
8889
using UnmarshalFuncWithSlice = int (*)(char **, char *, int, LogSlice *, LogEscapeType);
8990
using UnmarshalFuncWithMap = int (*)(char **, char *, int, const Ptr<LogFieldAliasMap> &);
9091
using SetFunc = void (LogAccess::*)(char *, int);
91-
using CustomMarshalFunc = int (*)(void *, char *);
92+
using CustomMarshalFunc = std::function<int(void *, char *)>;
9293
using CustomUnmarshalFunc = std::tuple<int, int> (*)(char **, char *, int);
9394

9495
using VarUnmarshalFuncSliceOnly = std::variant<UnmarshalFunc, UnmarshalFuncWithSlice>;
@@ -224,7 +225,7 @@ class LogField
224225
SetFunc m_set_func;
225226
TSMilestonesType milestone_from_m_name();
226227
int milestones_from_m_name(TSMilestonesType *m1, TSMilestonesType *m2);
227-
CustomMarshalFunc m_custom_marshal_func = nullptr;
228+
CustomMarshalFunc m_custom_marshal_func;
228229
CustomUnmarshalFunc m_custom_unmarshal_func = nullptr;
229230
std::vector<HeaderField> m_fallback_header_fields;
230231
std::unique_ptr<LogField> m_fallback_field;

include/ts/apidefs.h.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
*/
4444

4545
#include <cstdint>
46+
#include <functional>
4647
#include <memory>
4748
#include <vector>
4849
#include <netinet/in.h>
@@ -1161,7 +1162,7 @@ using TSFetchSM = struct tsapi_fetchsm *;
11611162
using TSThreadFunc = void *(*)(void *data);
11621163
using TSEventFunc = int (*)(TSCont contp, TSEvent event, void *edata);
11631164
using TSConfigDestroyFunc = void (*)(void *data);
1164-
using TSLogMarshalCallback = int (*)(TSHttpTxn, char *);
1165+
using TSLogMarshalCallback = std::function<int(TSHttpTxn, char *)>;
11651166
using TSLogUnmarshalCallback = std::tuple<int, int> (*)(char **, char *, int);
11661167

11671168
struct TSFetchEvent {

plugins/experimental/jax_fingerprint/config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ struct PluginConfig {
6262
std::string header_name = "";
6363
std::string via_header_name = "";
6464
std::string log_filename = "";
65+
std::string log_symbol = "";
6566
int user_arg_index = -1;
6667
TSCont handler = nullptr; // For remap plugin
6768
bool standalone = false;

plugins/experimental/jax_fingerprint/plugin.cc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ read_config_option(int argc, char const *argv[], PluginConfig &config)
5959
{"header", required_argument, nullptr, 'h'},
6060
{"via-header", required_argument, nullptr, 'v'},
6161
{"log-filename", required_argument, nullptr, 'f'},
62+
{"log-field", required_argument, nullptr, 'l'},
6263
{"servernames", required_argument, nullptr, 'S'},
6364
{nullptr, 0, nullptr, 0 }
6465
};
@@ -113,6 +114,9 @@ read_config_option(int argc, char const *argv[], PluginConfig &config)
113114
input.remove_prefix(pos == std::string_view::npos ? input.size() : pos + 1);
114115
}
115116
break;
117+
case 'l':
118+
config.log_symbol = {optarg, strlen(optarg)};
119+
break;
116120
case 0:
117121
case -1:
118122
break;
@@ -363,6 +367,28 @@ TSPluginInit(int argc, char const **argv)
363367
}
364368
}
365369

370+
if (!config->log_symbol.empty()) {
371+
std::string name = "jax_fingerprint-";
372+
name += config->method.name;
373+
TSLogFieldRegister(
374+
name.c_str(), config->log_symbol, TS_LOG_TYPE_STRING,
375+
[config](TSHttpTxn txnp, char *buf) -> int {
376+
void *container;
377+
if (config->method.type == Method::Type::CONNECTION_BASED) {
378+
container = TSHttpSsnClientVConnGet(TSHttpTxnSsnGet(txnp));
379+
} else {
380+
container = txnp;
381+
}
382+
JAxContext *ctx = get_user_arg(container, *config);
383+
if (ctx) {
384+
return TSLogStringMarshal(buf, ctx->get_fingerprint());
385+
} else {
386+
return TSLogStringMarshal(buf, "-");
387+
}
388+
},
389+
TSLogIntUnmarshal);
390+
}
391+
366392
if (reserve_user_arg(*config) == TS_ERROR) {
367393
TSError("[%s] Failed to reserve user arg index.", PLUGIN_NAME);
368394
return;
@@ -406,6 +432,12 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE
406432
return TS_ERROR;
407433
}
408434

435+
if (!config->log_symbol.empty()) {
436+
TSError("[%s] --log-field is not supported in remap.config. Use it in plugin.config instead.", PLUGIN_NAME);
437+
delete config;
438+
return TS_ERROR;
439+
}
440+
409441
// Create a log file
410442
if (!config->log_filename.empty()) {
411443
if (!create_log_file(config->log_filename, config->log_handle)) {

src/api/InkAPI.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9015,8 +9015,9 @@ TSLogFieldRegister(std::string_view name, std::string_view symbol, TSLogType typ
90159015
}
90169016
}
90179017

9018-
LogField *field = new LogField(name.data(), symbol.data(), static_cast<LogField::Type>(type),
9019-
reinterpret_cast<LogField::CustomMarshalFunc>(marshal_cb), unmarshal_cb);
9018+
LogField *field = new LogField(
9019+
name.data(), symbol.data(), static_cast<LogField::Type>(type),
9020+
[marshal_cb](void *sm, char *buf) -> int { return marshal_cb(reinterpret_cast<TSHttpTxn>(sm), buf); }, unmarshal_cb);
90209021
Log::global_field_list.add(field, false);
90219022
Log::field_symbol_hash.emplace(symbol.data(), field);
90229023

src/proxy/logging/LogAccess.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ LogAccess::marshal_ip(char *dest, sockaddr const *ip)
475475
}
476476

477477
int
478-
LogAccess::marshal_custom_field(char *buf, LogField::CustomMarshalFunc plugin_marshal_func)
478+
LogAccess::marshal_custom_field(char *buf, const LogField::CustomMarshalFunc &plugin_marshal_func)
479479
{
480480
int len = plugin_marshal_func(m_http_sm, buf);
481481
return LogAccess::padded_length(len);

src/proxy/logging/LogField.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ LogField::marshal_len(LogAccess *lad)
521521
}
522522

523523
if (m_container == NO_CONTAINER) {
524-
if (m_custom_marshal_func == nullptr) {
524+
if (!m_custom_marshal_func) {
525525
return (lad->*m_marshal_func)(nullptr);
526526
} else {
527527
return lad->marshal_custom_field(nullptr, m_custom_marshal_func);
@@ -630,7 +630,7 @@ LogField::marshal(LogAccess *lad, char *buf)
630630
}
631631

632632
if (m_container == NO_CONTAINER) {
633-
if (m_custom_marshal_func == nullptr) {
633+
if (!m_custom_marshal_func) {
634634
return (lad->*m_marshal_func)(buf);
635635
} else {
636636
return lad->marshal_custom_field(buf, m_custom_marshal_func);

tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ class JaxFingerprintTest:
4343
_client_counter: int = 0
4444

4545
def __init__(
46-
self, name: str, method: str, setup: str, mode: str = 'overwrite', http2: bool = False, servernames: str = '') -> None:
46+
self,
47+
name: str,
48+
method: str,
49+
setup: str,
50+
mode: str = 'overwrite',
51+
http2: bool = False,
52+
servernames: str = '',
53+
log_field: str = '') -> None:
4754
'''Configure test processes for the jax_fingerprint plugin.
4855
4956
:param name: Descriptive name for this test run.
@@ -60,6 +67,10 @@ def __init__(
6067
no context is created, and handle_read_request_hdr is a no-op.
6168
Only meaningful for CONNECTION_BASED methods (JA3/JA4) in global
6269
setup.
70+
:param log_field: Symbol name for --log-field option. When set,
71+
configures logging.yaml with a custom format using the symbol
72+
and verifies the fingerprint appears in the ATS access log.
73+
Only supported with global setup.
6374
6475
Method notes:
6576
- JA3 / JA4 are CONNECTION_BASED (triggered on TLS client hello)
@@ -86,6 +97,7 @@ def __init__(
8697
self._mode = mode
8798
self._http2 = http2
8899
self._servernames = servernames
100+
self._log_field = log_field
89101
# HTTP/2 always runs over TLS (h2 requires TLS).
90102
self._needs_tls = method in ('JA3', 'JA4') or http2
91103
self._replay_file = self._choose_replay_file()
@@ -98,6 +110,12 @@ def __init__(
98110
Test.AddAwaitFileContainsTestRun(
99111
f'Await jax_fingerprint.log for: {self._name}', self._ts.Disk.jax_log.AbsPath, self._method)
100112

113+
if self._log_field:
114+
log_field_path = os.path.join(self._ts.Variables.LOGDIR, 'jax_log_field.log')
115+
# Verify the log contains a fingerprint (not just a dash placeholder).
116+
Test.AddAwaitFileContainsTestRun(
117+
f'Await jax_log_field.log for: {self._name}', log_field_path, f'{self._method}: [a-z0-9]')
118+
101119
# ------------------------------------------------------------------
102120
# Helpers
103121
# ------------------------------------------------------------------
@@ -228,6 +246,8 @@ def _configure_trafficserver(self) -> None:
228246
global_args += f' --mode {self._mode}'
229247
if self._servernames:
230248
global_args += f' --servernames {self._servernames}'
249+
if self._log_field:
250+
global_args += f' --log-field {self._log_field}'
231251
self._ts.Disk.plugin_config.AddLine(f'jax_fingerprint.so {global_args}')
232252
self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax.server.test {backend}')
233253
if self._servernames:
@@ -267,6 +287,18 @@ def _configure_trafficserver(self) -> None:
267287
self._ts.Disk.remap_config.AddLine(
268288
f'map https://jax.server.test https://jax.backend.test:{server_port} {remap_line}')
269289

290+
if self._log_field:
291+
self._ts.Disk.logging_yaml.AddLines(
292+
f'''
293+
logging:
294+
formats:
295+
- name: jax_custom
296+
format: '{self._method}: %<{self._log_field}>'
297+
logs:
298+
- filename: jax_log_field
299+
format: jax_custom
300+
'''.split("\n"))
301+
270302
def _configure_client(self, tr: 'TestRun') -> None:
271303
'''Configure the verifier client.'''
272304
name = f'client{JaxFingerprintTest._client_counter}'
@@ -338,6 +370,13 @@ def _configure_client(self, tr: 'TestRun') -> None:
338370
# connection has a vconn context, so only that request gets headers.
339371
JaxFingerprintTest('Hybrid JA4 servernames', 'JA4', 'hybrid', servernames='jax.server.test')
340372

373+
# --- Custom log field (--log-field) -----------------------------------------
374+
375+
# Register a custom log field via --log-field and verify the fingerprint
376+
# appears in the ATS access log configured in logging.yaml.
377+
JaxFingerprintTest('Global JA4H log-field', 'JA4H', 'global', log_field='jaxja4h')
378+
JaxFingerprintTest('Global JA4 log-field', 'JA4', 'global', log_field='jaxja4')
379+
341380
# ======================================================================
342381
# All Methods Test - Verify shared context map works with multiple methods
343382
# ======================================================================

0 commit comments

Comments
 (0)