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

maskit 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 40b99c7b57 Add support for custom logging field (#12872)
40b99c7b57 is described below

commit 40b99c7b57eec10ff8e549a6d6d2b87663804eea
Author: Masakazu Kitajo <[email protected]>
AuthorDate: Tue Feb 24 14:40:31 2026 -0700

    Add support for custom logging field (#12872)
    
    This adds TSLogFieldRegister to enable plugins to add or redefine access 
log fields.
---
 .../api/functions/TSLifecycleHookAdd.en.rst        |   6 +
 .../api/functions/TSLogFieldRegister.en.rst        |  99 +++++++++
 doc/developer-guide/api/types/TSEvent.en.rst       |   4 +
 example/plugins/c-api/CMakeLists.txt               |   1 +
 .../c-api/custom_logfield/custom_logfield.cc       | 230 +++++++++++++++++++++
 include/proxy/logging/Log.h                        |   1 +
 include/proxy/logging/LogAccess.h                  |   4 +
 include/proxy/logging/LogField.h                   |   8 +
 include/ts/apidefs.h.in                            |  18 +-
 include/ts/ts.h                                    |  44 ++++
 src/api/InkAPI.cc                                  | 141 +++++++++++++
 src/proxy/logging/Log.cc                           |  19 +-
 src/proxy/logging/LogAccess.cc                     |   7 +
 src/proxy/logging/LogField.cc                      |  61 +++++-
 src/traffic_server/traffic_server.cc               |  10 +
 15 files changed, 640 insertions(+), 13 deletions(-)

diff --git a/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst 
b/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst
index e67ca32bc3..b9c3a70976 100644
--- a/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst
+++ b/doc/developer-guide/api/functions/TSLifecycleHookAdd.en.rst
@@ -120,6 +120,12 @@ Types
 
       Invoked with the event :enumerator:`TS_EVENT_LIFECYCLE_SHUTDOWN` and 
``nullptr`` data.
 
+   .. cpp:enumerator:: TS_LIFECYCLE_LOG_INITIALIZED_HOOK
+
+      Called after |TS| logging system is initialized but before logging 
configuration is loaded.
+
+      Invoked with the event :enumerator:`TS_EVENT_LIFECYCLE_LOG_INITIALIZED` 
and ``nullptr`` data.
+
 .. struct:: TSPluginMsg
 
    The data for the plugin message event :enumerator:`TS_EVENT_LIFECYCLE_MSG`.
diff --git a/doc/developer-guide/api/functions/TSLogFieldRegister.en.rst 
b/doc/developer-guide/api/functions/TSLogFieldRegister.en.rst
new file mode 100644
index 0000000000..4acdf40c84
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSLogFieldRegister.en.rst
@@ -0,0 +1,99 @@
+.. 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
+
+TSLogFieldRegister
+******************
+
+Registers a custom log field, or modifies an existing log field with a new 
definition.
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/ts.h>
+
+.. function:: TSReturnCode TSLogFieldRegister(std::string_view name, 
std::string_view symbol, TSLogType type, TSLogMarshalCallback marshal_cb, 
TSLogUnmarshalCallback unmarshal_cb, bool replace = false);
+
+.. enum:: TSLogType
+
+   Specify the type of a log field
+
+   .. enumerator:: TS_LOG_TYPE_INT
+
+      Integer field.
+
+   .. enumerator:: TS_LOG_TYPE_STRING
+
+      String field.
+
+   .. enumerator:: TS_LOG_TYPE_ADDR
+
+      Address field. It supports IPv4 address, IPv6 address, and Unix Domain 
Socket address (path).
+
+.. type:: int (*TSLogMarshalCallback)(TSHttpTxn, char *);
+
+   Callback signature for functions to marshal log fields.
+
+.. type:: std::tuple<int, int> (*TSLogUnmarshalCallback)(char **, char *, int);
+
+   Callback signature for functions to unmarshal log fields.
+
+.. function:: int TSLogStringMarshal(char *buf, std::string_view str);
+.. function:: int TSLogIntMarshal(char *buf, int64_t value);
+.. function:: int TSLogAddrMarshal(char *buf, sockaddr *addr);
+.. function:: std::tuple<int, int> TSLogStringUnmarshal(char **buf, char 
*dest, int len);
+.. function:: std::tuple<int, int> TSLogIntUnmarshal(char **buf, char *dest, 
int len);
+.. function:: std::tuple<int, int> TSLogAddrUnmarshal(char **buf, char *dest, 
int len);
+
+   Predefined marshaling and unmarshaling functions.
+
+Description
+===========
+
+The function registers or modifies a log field for access log. This is useful 
if you want to log something that |TS| does not expose,
+log plugin state, or redefine existing log fields.
+
+The `name` is a human friendly name, and only used for debugging. The `symbol` 
is the keyword you'd want to use on logging.yaml for
+the log field. It needs to be unique unless you are replacing an existing 
field by passing `true` to the optional argument
+`replace`, otherwise the API call fails.
+
+The `type` is the data type of a log field. You can log any data as a string 
value, but please note that aggregating functions such
+as AVG and SUM are only available for integer log fields.
+
+In many cases, you don't need to write code for marshaling and unmarshaling 
from scratch. The predefined functions are provided for
+your convenience, and you only needs to pass a value that you want to log,
+
+Example:
+
+    .. code-block:: cpp
+
+       TSLogFieldRegister("Example", "exmpl", TS_LOG_TYPE_INT,
+       [](TSHttpTxn txnp, char *buf) -> int {
+         return TSLogIntMarshal(buf, 123);
+       },
+       TSLogIntUnmarshal);
+
+Return Values
+=============
+
+:func:`TSLogFieldRegister` returns :enumerator:`TS_SUCCESS` if it successfully 
registeres a new field, or :enumerator:`TS_ERROR` if it
+fails due to symbol conflict. If :arg:`replace` is set to `true`, the function 
resolve the conflict by replacing the existing
+field definition with a new one, and returns :enumerator:`TS_SUCCESS`.
diff --git a/doc/developer-guide/api/types/TSEvent.en.rst 
b/doc/developer-guide/api/types/TSEvent.en.rst
index 1f06546358..9de2fe9d01 100644
--- a/doc/developer-guide/api/types/TSEvent.en.rst
+++ b/doc/developer-guide/api/types/TSEvent.en.rst
@@ -198,6 +198,10 @@ Enumeration Members
 
    The |TS| process has is shutting down.
 
+.. enumerator:: TS_EVENT_LIFECYCLE_LOG_INITIALIZED
+
+   The logging system is initialized.
+
 .. enumerator:: TS_EVENT_INTERNAL_60200
 
 .. enumerator:: TS_EVENT_INTERNAL_60201
diff --git a/example/plugins/c-api/CMakeLists.txt 
b/example/plugins/c-api/CMakeLists.txt
index 678fe416dd..65594cd391 100644
--- a/example/plugins/c-api/CMakeLists.txt
+++ b/example/plugins/c-api/CMakeLists.txt
@@ -65,3 +65,4 @@ add_atsplugin(statistic ./statistic/statistic.cc)
 add_atsplugin(protocol_stack ./protocol_stack/protocol_stack.cc)
 add_atsplugin(client_context_dump ./client_context_dump/client_context_dump.cc)
 target_link_libraries(client_context_dump PRIVATE OpenSSL::SSL 
libswoc::libswoc)
+add_atsplugin(custom_logfield ./custom_logfield/custom_logfield.cc)
diff --git a/example/plugins/c-api/custom_logfield/custom_logfield.cc 
b/example/plugins/c-api/custom_logfield/custom_logfield.cc
new file mode 100644
index 0000000000..975dd2900a
--- /dev/null
+++ b/example/plugins/c-api/custom_logfield/custom_logfield.cc
@@ -0,0 +1,230 @@
+/** @file
+
+  This plugin demonstrates custom log field registration and usage.
+  It populates custom log fields from per-transaction user arguments.
+
+  @section license License
+
+  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 <inttypes.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <ts/ts.h>
+#include <ts/remap.h>
+
+DbgCtl dbg_ctl{"custom_logfield"};
+
+char PLUGIN_NAME[]    = "custom_logfield";
+char VENDOR_NAME[]    = "Apache Software Foundation";
+char SUPPORT_EMAIL[]  = "[email protected]";
+char USER_ARG_CSTM[]  = "cstm_field";
+char USER_ARG_CSTMI[] = "cstmi_field";
+char USER_ARG_CSSN[]  = "cssn_field";
+
+int
+write_text_from_user_arg(TSHttpTxn txnp, char *buf, const char *user_arg_name)
+{
+  int len = 0;
+  int index;
+
+  if (TSUserArgIndexNameLookup(TS_USER_ARGS_TXN, user_arg_name, &index, 
nullptr) == TS_SUCCESS) {
+    Dbg(dbg_ctl, "User Arg Index: %d", index);
+    if (char *value = static_cast<char *>(TSUserArgGet(txnp, index)); value) {
+      Dbg(dbg_ctl, "Value: %s", value);
+      len = strlen(value);
+      if (buf) {
+        TSstrlcpy(buf, value, len + 1);
+      }
+    }
+  }
+  return len + 1;
+}
+
+int
+marshal_function_cstm(TSHttpTxn txnp, char *buf)
+{
+  if (buf) {
+    Dbg(dbg_ctl, "Marshaling a custom field cstm");
+  } else {
+    Dbg(dbg_ctl, "Marshaling a custom field cstm for size calculation");
+  }
+  return write_text_from_user_arg(txnp, buf, USER_ARG_CSTM);
+}
+
+int
+marshal_function_cssn(TSHttpTxn txnp, char *buf)
+{
+  if (buf) {
+    Dbg(dbg_ctl, "Marshaling a built-in field cssn");
+  } else {
+    Dbg(dbg_ctl, "Marshaling a built-in field cssn for size calculation");
+  }
+  return write_text_from_user_arg(txnp, buf, USER_ARG_CSSN);
+}
+
+int
+marshal_function_cstmi(TSHttpTxn txnp, char *buf)
+{
+  // This implementation is just to demonstrate marshaling an integer value.
+  // Predefined marshal function, TSLogIntMarshal, works for simple integer 
values
+
+  int index;
+
+  if (buf) {
+    Dbg(dbg_ctl, "Marshaling a custom field cstmi");
+  } else {
+    Dbg(dbg_ctl, "Marshaling a custom field cstmi for size calculation");
+  }
+
+  if (buf) {
+    if (TSUserArgIndexNameLookup(TS_USER_ARGS_TXN, USER_ARG_CSTMI, &index, 
nullptr) == TS_SUCCESS) {
+      Dbg(dbg_ctl, "User Arg Index: %d", index);
+      if (int64_t value = reinterpret_cast<int64_t>(TSUserArgGet(txnp, 
index)); value) {
+        Dbg(dbg_ctl, "Value: %" PRId64, value);
+        *(reinterpret_cast<int64_t *>(buf)) = value;
+      }
+    }
+  }
+  return sizeof(int64_t);
+}
+
+std::tuple<int, int>
+unmarshal_function_string(char **buf, char *dest, int len)
+{
+  Dbg(dbg_ctl, "Unmarshaling a string field");
+
+  // This implementation is just to demonstrate unmarshaling a string value.
+  // Predefined unmarshal function, TSLogStringUnmarshal, works for simple 
string values
+
+  int l = strlen(*buf);
+  Dbg(dbg_ctl, "Dest buf size: %d", len);
+  Dbg(dbg_ctl, "Unmarshaled value length: %d", l);
+  if (l < len) {
+    memcpy(dest, *buf, l);
+    Dbg(dbg_ctl, "Unmarshaled value: %.*s", l, dest);
+    return {
+      l, // The length of data read from buf
+      l  // The length of data written to dest
+    };
+  } else {
+    return {-1, -1};
+  }
+}
+
+int
+lifecycle_event_handler(TSCont /* contp ATS_UNUSED */, TSEvent event, void * 
/* edata ATS_UNUSED */)
+{
+  TSAssert(event == TS_EVENT_LIFECYCLE_LOG_INITIALIZED);
+
+  // This registers a custom log field "cstm".
+  Dbg(dbg_ctl, "Registering cstm log field");
+  TSLogFieldRegister("custom log field", "cstm", TS_LOG_TYPE_STRING, 
marshal_function_cstm, unmarshal_function_string);
+
+  // This replaces marshaling and unmarshaling functions for a built-in log 
field "cssn".
+  Dbg(dbg_ctl, "Overriding cssn log field");
+  TSLogFieldRegister("modified cssn", "cssn", TS_LOG_TYPE_STRING, 
marshal_function_cssn, TSLogStringUnmarshal, true);
+
+  // This registers a custom log field "cstmi"
+  Dbg(dbg_ctl, "Registering cstmi log field");
+  TSLogFieldRegister("custom integer log field", "cstmi", TS_LOG_TYPE_INT, 
marshal_function_cstmi, TSLogIntUnmarshal);
+
+  // This replaces marshaling and unmarshaling functions for a built-in log 
field "chi".
+  Dbg(dbg_ctl, "Overriding chi log field");
+  TSLogFieldRegister(
+    "modified cssn", "chi", TS_LOG_TYPE_ADDR,
+    [](TSHttpTxn /* txnp */, char *buf) -> int {
+      sockaddr_in addr;
+      addr.sin_family      = AF_INET;
+      addr.sin_port        = htons(80);
+      addr.sin_addr.s_addr = inet_addr("192.168.0.1");
+      return TSLogAddrMarshal(buf, reinterpret_cast<sockaddr *>(&addr));
+    },
+    TSLogAddrUnmarshal, true);
+
+  return TS_SUCCESS;
+}
+
+void
+TSPluginInit(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */)
+{
+  Dbg(dbg_ctl, "Initializing plugin");
+
+  TSPluginRegistrationInfo info = {PLUGIN_NAME, VENDOR_NAME, SUPPORT_EMAIL};
+  if (TSPluginRegister(&info) != TS_SUCCESS) {
+    TSError("[%s](%s) Plugin registration failed. \n", PLUGIN_NAME, 
__FUNCTION__);
+  }
+
+  TSCont cont = TSContCreate(lifecycle_event_handler, nullptr);
+  TSLifecycleHookAdd(TS_LIFECYCLE_LOG_INITIALIZED_HOOK, cont);
+
+  int argIndex;
+  TSUserArgIndexReserve(TS_USER_ARGS_TXN, USER_ARG_CSTM, "This is for cstm log 
field", &argIndex);
+  Dbg(dbg_ctl, "User Arg Index: %d", argIndex);
+  TSUserArgIndexReserve(TS_USER_ARGS_TXN, USER_ARG_CSSN, "This is for cssn log 
field", &argIndex);
+  Dbg(dbg_ctl, "User Arg Index: %d", argIndex);
+  TSUserArgIndexReserve(TS_USER_ARGS_TXN, USER_ARG_CSTMI, "This is for cstmi 
log field", &argIndex);
+  Dbg(dbg_ctl, "User Arg Index: %d", argIndex);
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *, char *, int)
+{
+  return TS_SUCCESS;
+}
+
+TSReturnCode
+TSRemapNewInstance(int, char **, void **, char *, int)
+{
+  return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *)
+{
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *, TSHttpTxn txn, TSRemapRequestInfo *)
+{
+  Dbg(dbg_ctl, "Remapping");
+
+  int index;
+
+  // Store a string value for cstm field
+  if (TSUserArgIndexNameLookup(TS_USER_ARGS_TXN, USER_ARG_CSTM, &index, 
nullptr) == TS_SUCCESS) {
+    Dbg(dbg_ctl, "User Arg Index: %d", index);
+    TSUserArgSet(txn, index, const_cast<char *>("abc"));
+  }
+
+  // Store a string value for cssn field
+  if (TSUserArgIndexNameLookup(TS_USER_ARGS_TXN, USER_ARG_CSSN, &index, 
nullptr) == TS_SUCCESS) {
+    Dbg(dbg_ctl, "User Arg Index: %d", index);
+    TSUserArgSet(txn, index, const_cast<char *>("xyz"));
+  }
+
+  // Store an integer value for cstmi field
+  if (TSUserArgIndexNameLookup(TS_USER_ARGS_TXN, USER_ARG_CSTMI, &index, 
nullptr) == TS_SUCCESS) {
+    Dbg(dbg_ctl, "User Arg Index: %d", index);
+    TSUserArgSet(txn, index, reinterpret_cast<void *>(43));
+  }
+
+  return TSREMAP_NO_REMAP;
+}
diff --git a/include/proxy/logging/Log.h b/include/proxy/logging/Log.h
index 4cf40db096..ef7df731c0 100644
--- a/include/proxy/logging/Log.h
+++ b/include/proxy/logging/Log.h
@@ -155,6 +155,7 @@ public:
   // main interface
   static void init(int configFlags = 0);
   static void init_fields();
+  static void load_config();
 
   static bool
   transaction_logging_enabled()
diff --git a/include/proxy/logging/LogAccess.h 
b/include/proxy/logging/LogAccess.h
index c50bb359c5..1a799ca3b0 100644
--- a/include/proxy/logging/LogAccess.h
+++ b/include/proxy/logging/LogAccess.h
@@ -307,6 +307,10 @@ public:
   int marshal_milestones_csv(char *buf);
 
   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);
+
   //
   // unmarshalling routines
   //
diff --git a/include/proxy/logging/LogField.h b/include/proxy/logging/LogField.h
index dcc5b23bb0..b883b7c558 100644
--- a/include/proxy/logging/LogField.h
+++ b/include/proxy/logging/LogField.h
@@ -26,6 +26,7 @@
 #include <string_view>
 #include <string>
 #include <variant>
+#include <tuple>
 
 #include "tscore/ink_inet.h"
 #include "tscore/ink_platform.h"
@@ -84,6 +85,8 @@ 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 CustomUnmarshalFunc    = std::tuple<int, int> (*)(char **, char *, 
int);
 
   using VarUnmarshalFuncSliceOnly = std::variant<UnmarshalFunc, 
UnmarshalFuncWithSlice>;
   using VarUnmarshalFunc          = std::variant<decltype(nullptr), 
UnmarshalFunc, UnmarshalFuncWithSlice, UnmarshalFuncWithMap>;
@@ -132,6 +135,8 @@ public:
   LogField(const char *name, const char *symbol, Type type, MarshalFunc 
marshal, UnmarshalFuncWithMap unmarshal,
            const Ptr<LogFieldAliasMap> &map, SetFunc _setFunc = nullptr);
 
+  LogField(const char *name, const char *symbol, Type type, CustomMarshalFunc 
custom_marshal, CustomUnmarshalFunc custom_unmarshal);
+
   LogField(const char *field, Container container);
   LogField(const LogField &rhs);
   ~LogField();
@@ -207,6 +212,8 @@ private:
   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;
 
 public:
   LINK(LogField, link);
@@ -234,6 +241,7 @@ public:
 
   void      clear();
   void      add(LogField *field, bool copy = true);
+  void      remove(LogField *field);
   LogField *find_by_name(const char *name) const;
   LogField *find_by_symbol(const char *symbol) const;
   unsigned  marshal_len(LogAccess *lad);
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index 5515aa7665..c979ac8e94 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -46,6 +46,7 @@
 #include <netinet/in.h>
 #include <sys/types.h>
 #include <sys/socket.h>
+#include <tuple>
 
 /** Apply printf format string compile-time argument checking to a function.
  *
@@ -361,6 +362,7 @@ enum TSEvent {
   TS_EVENT_LIFECYCLE_MSG                        = 60105,
   TS_EVENT_LIFECYCLE_TASK_THREADS_READY         = 60106,
   TS_EVENT_LIFECYCLE_SHUTDOWN                   = 60107,
+  TS_EVENT_LIFECYCLE_LOG_INITIALIZED            = 60108,
 
   TS_EVENT_INTERNAL_60200    = 60200,
   TS_EVENT_INTERNAL_60201    = 60201,
@@ -578,6 +580,7 @@ enum TSLifecycleHookID {
   TS_LIFECYCLE_TASK_THREADS_READY_HOOK,
   TS_LIFECYCLE_SHUTDOWN_HOOK,
   TS_LIFECYCLE_SSL_SECRET_HOOK,
+  TS_LIFECYCLE_LOG_INITIALIZED_HOOK,
   TS_LIFECYCLE_LAST_HOOK,
 };
 
@@ -1084,9 +1087,11 @@ using TSRemapPluginInfo  = struct 
tsapi_remap_plugin_info *;
 
 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 TSThreadFunc           = void *(*)(void *data);
+using TSEventFunc            = int (*)(TSCont contp, TSEvent event, void 
*edata);
+using TSConfigDestroyFunc    = void (*)(void *data);
+using TSLogMarshalCallback   = int (*)(TSHttpTxn, char *);
+using TSLogUnmarshalCallback = std::tuple<int, int> (*)(char **, char *, int);
 
 struct TSFetchEvent {
   int success_event_id;
@@ -1571,6 +1576,13 @@ struct TSResponseAction {
   bool        no_cache;
 };
 
+enum TSLogType {
+  TS_LOG_TYPE_INT,
+  // DINT is omitted from the public API for now, until we decide whether we 
keep the type
+  TS_LOG_TYPE_STRING = 2,
+  TS_LOG_TYPE_ADDR   = 3,
+};
+
 /* --------------------------------------------------------------------------
    Init */
 
diff --git a/include/ts/ts.h b/include/ts/ts.h
index d62d5d8156..c63febb360 100644
--- a/include/ts/ts.h
+++ b/include/ts/ts.h
@@ -3224,3 +3224,47 @@ TSReturnCode TSVConnPPInfoGet(TSVConn vconn, uint16_t 
key, const char **value, i
 
 */
 TSReturnCode TSVConnPPInfoIntGet(TSVConn vconn, uint16_t key, TSMgmtInt 
*value);
+
+/**
+   Registers a custom log field, or modifies an existing log field with a new 
definition.
+
+   @param name a human friendly name
+   @param symbol a symbol to use on the config file
+   @param type a type of the new log field
+   @param marshal_cb a callback function to marshal log  value
+   @param unmarshal_cb a callback function to unmarshal log value
+   @param replace a flag to allow replacing an existing log field
+
+   @return @c TS_SCCESS if the registration successes, TS_ERROR otherwise
+*/
+TSReturnCode TSLogFieldRegister(std::string_view name, std::string_view 
symbol, TSLogType type, TSLogMarshalCallback marshal_cb,
+                                TSLogUnmarshalCallback unmarshal_cb, bool 
replace = false);
+/**
+   Helper function to marshal a string
+*/
+int TSLogStringMarshal(char *buf, std::string_view str);
+
+/**
+   Helper function to marshal an integer
+*/
+int TSLogIntMarshal(char *buf, int64_t value);
+
+/**
+   Helper function to marshal an address
+*/
+int TSLogAddrMarshal(char *buf, sockaddr *addr);
+
+/**
+   Helper function to unmarshal a string
+*/
+std::tuple<int, int> TSLogStringUnmarshal(char **buf, char *dest, int len);
+
+/**
+   Helper function to unmarshal an integer
+*/
+std::tuple<int, int> TSLogIntUnmarshal(char **buf, char *dest, int len);
+
+/**
+   Helper function to unmarshal an address
+*/
+std::tuple<int, int> TSLogAddrUnmarshal(char **buf, char *dest, int len);
diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc
index 462d15cf38..1a4b734e28 100644
--- a/src/api/InkAPI.cc
+++ b/src/api/InkAPI.cc
@@ -26,6 +26,7 @@
 #include <unordered_map>
 #include <string_view>
 #include <string>
+#include <charconv>
 
 #include "iocore/net/NetVConnection.h"
 #include "iocore/net/NetHandler.h"
@@ -9017,3 +9018,143 @@ TSConnectionLimitExemptListClear()
 {
   ConnectionTracker::clear_client_exempt_list();
 }
+
+TSReturnCode
+TSLogFieldRegister(std::string_view name, std::string_view symbol, TSLogType 
type, TSLogMarshalCallback marshal_cb,
+                   TSLogUnmarshalCallback unmarshal_cb, bool replace)
+{
+  if (auto ite = Log::field_symbol_hash.find(symbol.data()); ite != 
Log::field_symbol_hash.end()) {
+    if (replace) {
+      // Symbol is registered and the plugin wants to replace it.
+      // Need to unregister the existing entry first.
+      Log::global_field_list.remove(ite->second);
+      Log::field_symbol_hash.erase(ite);
+    } else {
+      // Symbol conflict.
+      return TS_ERROR;
+    }
+  }
+
+  LogField *field = new LogField(name.data(), symbol.data(), 
static_cast<LogField::Type>(type),
+                                 
reinterpret_cast<LogField::CustomMarshalFunc>(marshal_cb), unmarshal_cb);
+  Log::global_field_list.add(field, false);
+  Log::field_symbol_hash.emplace(symbol.data(), field);
+
+  return TS_SUCCESS;
+}
+
+int
+TSLogStringMarshal(char *buf, std::string_view str)
+{
+  if (buf) {
+    ink_strlcpy(buf, str.data(), str.length() + 1);
+  }
+  return str.length() + 1;
+}
+
+std::tuple<int, int>
+TSLogStringUnmarshal(char **buf, char *dest, int len)
+{
+  // We cannot use LogAccess::unmarshal_str, etc. here because those internal
+  // functions take care of log buffer alignment. This function needs to be
+  // implemented as if it's a piece of code in plugin code, which is unaware
+  // of the alignment.
+  if (int l = strlen(*buf); l < len) {
+    memcpy(dest, *buf, l);
+    return {l, l};
+  } else {
+    return {-1, -1};
+  }
+}
+
+int
+TSLogIntMarshal(char *buf, int64_t value)
+{
+  if (buf) {
+    *(reinterpret_cast<int64_t *>(buf)) = value;
+  }
+  return sizeof(int64_t);
+}
+
+std::tuple<int, int>
+TSLogIntUnmarshal(char **buf, char *dest, int len)
+{
+  int64_t val     = *(reinterpret_cast<int64_t *>(*buf));
+  auto [end, err] = std::to_chars(dest, dest + len, val);
+  if (err == std::errc()) {
+    *end = '\0';
+    return {sizeof(uint64_t), end - dest};
+  }
+
+  return {-1, -1};
+}
+
+int
+TSLogAddrMarshal(char *buf, sockaddr *addr)
+{
+  LogFieldIpStorage data;
+  int               len = sizeof(data._ip);
+
+  if (nullptr == addr) {
+    data._ip._family = AF_UNSPEC;
+  } else if (ats_is_ip4(addr)) {
+    if (buf) {
+      data._ip4._family = AF_INET;
+      data._ip4._addr   = ats_ip4_addr_cast(addr);
+    }
+    len = sizeof(data._ip4);
+  } else if (ats_is_ip6(addr)) {
+    if (buf) {
+      data._ip6._family = AF_INET6;
+      data._ip6._addr   = ats_ip6_addr_cast(addr);
+    }
+    len = sizeof(data._ip6);
+  } else if (ats_is_unix(addr)) {
+    if (buf) {
+      data._un._family = AF_UNIX;
+      strncpy(data._un._path, ats_unix_cast(addr)->sun_path, TS_UNIX_SIZE);
+    }
+    len = sizeof(data._un);
+  } else {
+    data._ip._family = AF_UNSPEC;
+  }
+
+  if (buf) {
+    memcpy(buf, &data, len);
+  }
+  return len;
+}
+
+std::tuple<int, int>
+TSLogAddrUnmarshal(char **buf, char *dest, int len)
+{
+  IpEndpoint endpoint;
+  int        read_len = sizeof(LogFieldIp);
+
+  LogFieldIp *raw = reinterpret_cast<LogFieldIp *>(*buf);
+  if (AF_INET == raw->_family) {
+    LogFieldIp4 *ip4 = static_cast<LogFieldIp4 *>(raw);
+    ats_ip4_set(&endpoint, ip4->_addr);
+    read_len = sizeof(*ip4);
+  } else if (AF_INET6 == raw->_family) {
+    LogFieldIp6 *ip6 = static_cast<LogFieldIp6 *>(raw);
+    ats_ip6_set(&endpoint, ip6->_addr);
+    read_len = sizeof(*ip6);
+  } else if (AF_UNIX == raw->_family) {
+    LogFieldUn *un = static_cast<LogFieldUn *>(raw);
+    ats_unix_set(&endpoint, un->_path, TS_UNIX_SIZE);
+    read_len = sizeof(*un);
+  } else {
+    ats_ip_invalidate(&endpoint);
+  }
+
+  if (!ats_is_ip(&endpoint) && !ats_is_unix(&endpoint)) {
+    dest[0] = '0';
+    dest[1] = '\0';
+    return {-1, 1};
+  } else if (ats_ip_ntop(&endpoint, dest, len)) {
+    return {read_len, static_cast<int>(::strlen(dest))};
+  }
+
+  return {-1, -1};
+}
diff --git a/src/proxy/logging/Log.cc b/src/proxy/logging/Log.cc
index 3bca7c5333..29b0949698 100644
--- a/src/proxy/logging/Log.cc
+++ b/src/proxy/logging/Log.cc
@@ -1148,13 +1148,6 @@ Log::init(int flags)
   }
 
   init_fields();
-  if (!(config_flags & LOGCAT)) {
-    RecRegisterConfigUpdateCb("proxy.config.log.logging_enabled", 
&Log::handle_logging_mode_change, nullptr);
-
-    Dbg(dbg_ctl_log_config, "Log::init(): logging_mode = %d init status = %d", 
logging_mode, init_status);
-    config->init();
-    init_when_enabled();
-  }
 }
 
 void
@@ -1179,6 +1172,18 @@ Log::init_when_enabled()
   }
 }
 
+void
+Log::load_config()
+{
+  if (!(config_flags & LOGCAT)) {
+    RecRegisterConfigUpdateCb("proxy.config.log.logging_enabled", 
&Log::handle_logging_mode_change, nullptr);
+
+    Dbg(dbg_ctl_log_config, "Log::init(): logging_mode = %d init status = %d", 
logging_mode, init_status);
+    config->init();
+    init_when_enabled();
+  }
+}
+
 void
 Log::create_threads()
 {
diff --git a/src/proxy/logging/LogAccess.cc b/src/proxy/logging/LogAccess.cc
index 336238c345..7d2bcb54da 100644
--- a/src/proxy/logging/LogAccess.cc
+++ b/src/proxy/logging/LogAccess.cc
@@ -474,6 +474,13 @@ LogAccess::marshal_ip(char *dest, sockaddr const *ip)
   return INK_ALIGN_DEFAULT(len);
 }
 
+int
+LogAccess::marshal_custom_field(char *buf, LogField::CustomMarshalFunc 
plugin_marshal_func)
+{
+  int len = plugin_marshal_func(m_http_sm, buf);
+  return LogAccess::padded_length(len);
+}
+
 inline int
 LogAccess::unmarshal_with_map(int64_t code, char *dest, int len, const 
Ptr<LogFieldAliasMap> &map, const char *msg)
 {
diff --git a/src/proxy/logging/LogField.cc b/src/proxy/logging/LogField.cc
index 039aca32a7..914363ced6 100644
--- a/src/proxy/logging/LogField.cc
+++ b/src/proxy/logging/LogField.cc
@@ -287,6 +287,33 @@ LogField::LogField(const char *name, const char *symbol, 
Type type, MarshalFunc
                   strcmp(m_symbol, "cqtn") == 0 || strcmp(m_symbol, "cqtd") == 
0 || strcmp(m_symbol, "cqtt") == 0);
 }
 
+LogField::LogField(const char *name, const char *symbol, Type type, 
CustomMarshalFunc custom_marshal,
+                   CustomUnmarshalFunc custom_unmarshal)
+  : m_name(ats_strdup(name)),
+    m_symbol(ats_strdup(symbol)),
+    m_type(type),
+    m_container(NO_CONTAINER),
+    m_marshal_func(nullptr),
+    m_unmarshal_func(VarUnmarshalFunc(nullptr)),
+    m_agg_op(NO_AGGREGATE),
+    m_agg_cnt(0),
+    m_agg_val(0),
+    m_milestone1(TS_MILESTONE_LAST_ENTRY),
+    m_milestone2(TS_MILESTONE_LAST_ENTRY),
+    m_time_field(false),
+    m_alias_map(nullptr),
+    m_set_func(nullptr),
+    m_custom_marshal_func(custom_marshal),
+    m_custom_unmarshal_func(custom_unmarshal)
+{
+  ink_assert(m_name != nullptr);
+  ink_assert(m_symbol != nullptr);
+  ink_assert(m_type >= 0 && m_type < N_TYPES);
+
+  m_time_field = (strcmp(m_symbol, "cqts") == 0 || strcmp(m_symbol, "cqth") == 
0 || strcmp(m_symbol, "cqtq") == 0 ||
+                  strcmp(m_symbol, "cqtn") == 0 || strcmp(m_symbol, "cqtd") == 
0 || strcmp(m_symbol, "cqtt") == 0);
+}
+
 TSMilestonesType
 LogField::milestone_from_m_name()
 {
@@ -413,7 +440,9 @@ LogField::LogField(const LogField &rhs)
     m_milestone2(rhs.m_milestone2),
     m_time_field(rhs.m_time_field),
     m_alias_map(rhs.m_alias_map),
-    m_set_func(rhs.m_set_func)
+    m_set_func(rhs.m_set_func),
+    m_custom_marshal_func(rhs.m_custom_marshal_func),
+    m_custom_unmarshal_func(rhs.m_custom_unmarshal_func)
 {
   ink_assert(m_name != nullptr);
   ink_assert(m_symbol != nullptr);
@@ -441,7 +470,11 @@ unsigned
 LogField::marshal_len(LogAccess *lad)
 {
   if (m_container == NO_CONTAINER) {
-    return (lad->*m_marshal_func)(nullptr);
+    if (m_custom_marshal_func == nullptr) {
+      return (lad->*m_marshal_func)(nullptr);
+    } else {
+      return lad->marshal_custom_field(nullptr, m_custom_marshal_func);
+    }
   }
 
   switch (m_container) {
@@ -523,7 +556,11 @@ unsigned
 LogField::marshal(LogAccess *lad, char *buf)
 {
   if (m_container == NO_CONTAINER) {
-    return (lad->*m_marshal_func)(buf);
+    if (m_custom_marshal_func == nullptr) {
+      return (lad->*m_marshal_func)(buf);
+    } else {
+      return lad->marshal_custom_field(buf, m_custom_marshal_func);
+    }
   }
 
   switch (m_container) {
@@ -615,6 +652,11 @@ LogField::unmarshal(char **buf, char *dest, int len, 
LogEscapeType escape_type)
                      [&](UnmarshalFuncWithMap f) -> unsigned { return 
(*f)(buf, dest, len, m_alias_map); },
                      [&](UnmarshalFunc f) -> unsigned { return (*f)(buf, dest, 
len); },
                      [&](decltype(nullptr)) -> unsigned {
+                       if (m_custom_unmarshal_func) {
+                         auto [read_len, written_len]  = 
m_custom_unmarshal_func(buf, dest, len);
+                         *buf                         += 
LogAccess::padded_length(read_len);
+                         return written_len;
+                       }
                        ink_assert(false);
                        return 0;
                      }},
@@ -783,6 +825,19 @@ LogFieldList::add(LogField *field, bool copy)
   }
 }
 
+void
+LogFieldList::remove(LogField *field)
+{
+  ink_assert(field != nullptr);
+
+  if (field->type() == LogField::sINT) {
+    m_marshal_len -= INK_MIN_ALIGN;
+  }
+  m_field_list.remove(field);
+
+  delete field;
+}
+
 LogField *
 LogFieldList::find_by_name(const char *name) const
 {
diff --git a/src/traffic_server/traffic_server.cc 
b/src/traffic_server/traffic_server.cc
index 811539f143..9a24991059 100644
--- a/src/traffic_server/traffic_server.cc
+++ b/src/traffic_server/traffic_server.cc
@@ -2228,6 +2228,16 @@ main(int /* argc ATS_UNUSED */, const char **argv)
       pluginInitCheck.notify_one();
     }
 
+    // Give plugins a chance to customize log fields
+    APIHook *hook = g_lifecycle_hooks->get(TS_LIFECYCLE_LOG_INITIALIZED_HOOK);
+    while (hook) {
+      hook->invoke(TS_EVENT_LIFECYCLE_LOG_INITIALIZED, nullptr);
+      hook = hook->next();
+    }
+
+    // Log config needs to be loaded after the custom field registration
+    Log::load_config();
+
     if (IpAllow::has_no_rules()) {
       Error("No ip_allow.yaml entries found.  All requests will be denied!");
     }

Reply via email to