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

shreemaan-abhishek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new b79329d86 feat: add log_format_extra to enrich the default logger 
format (#13568)
b79329d86 is described below

commit b79329d86fe0744ed885a4ac558c369a511bb9ad
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Sat Jun 20 11:42:13 2026 +0800

    feat: add log_format_extra to enrich the default logger format (#13568)
---
 apisix/balancer.lua                            |  10 +
 apisix/cli/ngx_tpl.lua                         |   1 +
 apisix/core/ctx.lua                            |   3 +
 apisix/plugins/clickhouse-logger.lua           |   4 +
 apisix/plugins/elasticsearch-logger.lua        |   4 +
 apisix/plugins/file-logger.lua                 |   4 +
 apisix/plugins/google-cloud-logging.lua        |  15 +-
 apisix/plugins/http-logger.lua                 |   4 +
 apisix/plugins/kafka-logger.lua                |   4 +
 apisix/plugins/loggly.lua                      |   4 +
 apisix/plugins/loki-logger.lua                 |   4 +
 apisix/plugins/rocketmq-logger.lua             |   4 +
 apisix/plugins/skywalking-logger.lua           |   4 +
 apisix/plugins/sls-logger.lua                  |   4 +
 apisix/plugins/splunk-hec-logging.lua          |  15 +-
 apisix/plugins/syslog.lua                      |   4 +
 apisix/plugins/tcp-logger.lua                  |   4 +
 apisix/plugins/tencent-cloud-cls.lua           |   4 +
 apisix/plugins/udp-logger.lua                  |   4 +
 apisix/utils/log-util.lua                      |  29 +-
 docs/en/latest/apisix-variable.md              |   1 +
 docs/en/latest/plugins/clickhouse-logger.md    |   2 +
 docs/en/latest/plugins/elasticsearch-logger.md |   2 +
 docs/en/latest/plugins/google-cloud-logging.md |   2 +
 docs/en/latest/plugins/http-logger.md          |   2 +
 docs/en/latest/plugins/kafka-logger.md         |   2 +
 docs/en/latest/plugins/loggly.md               |   2 +
 docs/en/latest/plugins/loki-logger.md          |   6 +-
 docs/en/latest/plugins/rocketmq-logger.md      |   2 +
 docs/en/latest/plugins/skywalking-logger.md    |   2 +
 docs/en/latest/plugins/sls-logger.md           |   2 +
 docs/en/latest/plugins/splunk-hec-logging.md   |   2 +
 docs/en/latest/plugins/syslog.md               |   2 +
 docs/en/latest/plugins/tcp-logger.md           |   2 +
 docs/en/latest/plugins/tencent-cloud-cls.md    |   2 +
 docs/en/latest/plugins/udp-logger.md           |   2 +
 t/APISIX.pm                                    |   3 +-
 t/core/ctx.t                                   |  53 ++++
 t/plugin/file-logger.t                         | 417 +++++++++++++++++++++++++
 t/plugin/loki-logger.t                         |  94 ++++++
 t/plugin/splunk-hec-logging.t                  |  68 ++++
 41 files changed, 793 insertions(+), 6 deletions(-)

diff --git a/apisix/balancer.lua b/apisix/balancer.lua
index 7a358dadf..ce8277b4f 100644
--- a/apisix/balancer.lua
+++ b/apisix/balancer.lua
@@ -252,6 +252,11 @@ local function pick_server(route, ctx)
         local node = up_conf.nodes[1]
         ctx.balancer_ip = node.host
         ctx.balancer_port = node.port
+        ctx.upstream_unresolved_host = node.domain or node.host
+        -- also expose it as an nginx var so it can be used in the access log 
(http only)
+        if is_http and ctx.var then
+            ctx.var.upstream_unresolved_host = ctx.upstream_unresolved_host
+        end
         node.upstream_host = parse_server_for_upstream_host(node, 
ctx.upstream_scheme)
         return node
     end
@@ -337,6 +342,11 @@ local function pick_server(route, ctx)
     res.domain = domain
     ctx.balancer_ip = res.host
     ctx.balancer_port = res.port
+    ctx.upstream_unresolved_host = res.domain or res.host
+    -- also expose it as an nginx var so it can be used in the access log 
(http only)
+    if is_http and ctx.var then
+        ctx.var.upstream_unresolved_host = ctx.upstream_unresolved_host
+    end
     ctx.server_picker = server_picker
     res.upstream_host = parse_server_for_upstream_host(res, 
ctx.upstream_scheme)
 
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index d95046efe..52574e28d 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -810,6 +810,7 @@ http {
 
             set $upstream_scheme             'http';
             set $upstream_host               $http_host;
+            set $upstream_unresolved_host    '';
             set $upstream_uri                '';
             set $request_line                '';
             set $ctx_ref                     '';
diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua
index 58ee7c6b6..1827254b6 100644
--- a/apisix/core/ctx.lua
+++ b/apisix/core/ctx.lua
@@ -190,6 +190,7 @@ do
     local ngx_var_names = {
         upstream_scheme            = true,
         upstream_host              = true,
+        upstream_unresolved_host   = true,
         upstream_upgrade           = true,
         upstream_connection        = true,
         upstream_uri               = true,
@@ -244,6 +245,8 @@ do
         route_name = true,
         service_id = true,
         service_name = true,
+        -- the upstream host before DNS resolution (configured domain/host)
+        upstream_unresolved_host = true,
     }
 
     local mt = {
diff --git a/apisix/plugins/clickhouse-logger.lua 
b/apisix/plugins/clickhouse-logger.lua
index 99135f707..fd44e32fc 100644
--- a/apisix/plugins/clickhouse-logger.lua
+++ b/apisix/plugins/clickhouse-logger.lua
@@ -42,6 +42,7 @@ local schema = {
         name = {type = "string", default = "clickhouse logger"},
         ssl_verify = {type = "boolean", default = true},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
             type = "array",
@@ -72,6 +73,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/elasticsearch-logger.lua 
b/apisix/plugins/elasticsearch-logger.lua
index e03d75c72..736b03219 100644
--- a/apisix/plugins/elasticsearch-logger.lua
+++ b/apisix/plugins/elasticsearch-logger.lua
@@ -55,6 +55,7 @@ local schema = {
             required = {"index"}
         },
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         auth = {
             type = "object",
             properties = {
@@ -121,6 +122,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/file-logger.lua b/apisix/plugins/file-logger.lua
index a8f809de7..afe73d662 100644
--- a/apisix/plugins/file-logger.lua
+++ b/apisix/plugins/file-logger.lua
@@ -34,6 +34,7 @@ local schema = {
             type = "string"
         },
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
             type = "array",
@@ -71,6 +72,9 @@ local metadata_schema = {
         },
         log_format = {
             type = "object"
+        },
+        log_format_extra = {
+            type = "object"
         }
     }
 }
diff --git a/apisix/plugins/google-cloud-logging.lua 
b/apisix/plugins/google-cloud-logging.lua
index 39202e521..6133293d2 100644
--- a/apisix/plugins/google-cloud-logging.lua
+++ b/apisix/plugins/google-cloud-logging.lua
@@ -18,6 +18,7 @@
 local core            = require("apisix.core")
 local plugin          = require("apisix.plugin")
 local tostring        = tostring
+local pairs           = pairs
 local http            = require("resty.http")
 local log_util        = require("apisix.utils.log-util")
 local bp_manager_mod  = require("apisix.utils.batch-processor-manager")
@@ -98,6 +99,7 @@ local schema = {
             default = "apisix.apache.org%2Flogs"
         },
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
     },
     oneOf = {
         { required = { "auth_config" } },
@@ -109,6 +111,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
@@ -190,7 +195,7 @@ end
 
 
 local function get_logger_entry(conf, ctx, oauth)
-    local entry, customized = log_util.get_log_entry(plugin_name, conf, ctx)
+    local entry, customized, extra = log_util.get_log_entry(plugin_name, conf, 
ctx)
     local google_entry
     if not customized then
         google_entry = {
@@ -210,6 +215,14 @@ local function get_logger_entry(conf, ctx, oauth)
                 service_id = entry.service_id,
             },
         }
+        -- the fixed payload above drops everything else, so add 
log_format_extra
+        if extra then
+            for k, v in pairs(extra) do
+                if google_entry.jsonPayload[k] == nil then
+                    google_entry.jsonPayload[k] = v
+                end
+            end
+        end
     else
         google_entry = {
             jsonPayload = entry,
diff --git a/apisix/plugins/http-logger.lua b/apisix/plugins/http-logger.lua
index cc8481344..a57eb3564 100644
--- a/apisix/plugins/http-logger.lua
+++ b/apisix/plugins/http-logger.lua
@@ -34,6 +34,7 @@ local schema = {
         auth_header = {type = "string"},
         timeout = {type = "integer", minimum = 1, default = 3},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
             type = "array",
@@ -64,6 +65,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/kafka-logger.lua b/apisix/plugins/kafka-logger.lua
index 014898c5e..845fa684b 100644
--- a/apisix/plugins/kafka-logger.lua
+++ b/apisix/plugins/kafka-logger.lua
@@ -40,6 +40,7 @@ local schema = {
             enum = {"default", "origin"},
         },
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         -- deprecated, use "brokers" instead
         broker_list = {
             type = "object",
@@ -146,6 +147,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/loggly.lua b/apisix/plugins/loggly.lua
index d7d792764..112f72e12 100644
--- a/apisix/plugins/loggly.lua
+++ b/apisix/plugins/loggly.lua
@@ -95,6 +95,7 @@ local schema = {
             default = true
         },
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         severity_map = {
             type = "object",
             description = "upstream response code vs syslog severity mapping",
@@ -143,6 +144,9 @@ local metadata_schema = {
             minimum = 1,
             default= defaults.timeout
         },
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object",
         }
diff --git a/apisix/plugins/loki-logger.lua b/apisix/plugins/loki-logger.lua
index b107ec10a..4e584a827 100644
--- a/apisix/plugins/loki-logger.lua
+++ b/apisix/plugins/loki-logger.lua
@@ -91,6 +91,7 @@ local schema = {
 
         -- logger related configurations
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
             type = "array",
@@ -120,6 +121,9 @@ local metadata_schema = {
         log_format = {
             type = "object"
         },
+        log_format_extra = {
+            type = "object"
+        },
         max_pending_entries = {
             type = "integer",
             description = "maximum number of pending entries in the batch 
processor",
diff --git a/apisix/plugins/rocketmq-logger.lua 
b/apisix/plugins/rocketmq-logger.lua
index 6610fb33e..9059e9ee2 100644
--- a/apisix/plugins/rocketmq-logger.lua
+++ b/apisix/plugins/rocketmq-logger.lua
@@ -48,6 +48,7 @@ local schema = {
         key = {type = "string"},
         tag = {type = "string"},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         timeout = {type = "integer", minimum = 1, default = 3},
         use_tls = {type = "boolean", default = false},
         access_key = {type = "string", default = ""},
@@ -78,6 +79,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/skywalking-logger.lua 
b/apisix/plugins/skywalking-logger.lua
index 841a3e606..712dca945 100644
--- a/apisix/plugins/skywalking-logger.lua
+++ b/apisix/plugins/skywalking-logger.lua
@@ -37,6 +37,7 @@ local schema = {
         service_name = {type = "string", default = "APISIX"},
         service_instance_name = {type = "string", default = "APISIX Instance 
Name"},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         timeout = {type = "integer", minimum = 1, default = 3},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
@@ -64,6 +65,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/sls-logger.lua b/apisix/plugins/sls-logger.lua
index c482f51ae..1f0c6280f 100644
--- a/apisix/plugins/sls-logger.lua
+++ b/apisix/plugins/sls-logger.lua
@@ -52,6 +52,7 @@ local schema = {
         max_resp_body_bytes = {type = "integer", minimum = 1, default = 
524288},
         timeout = {type = "integer", minimum = 1, default= 5000},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         host = {type = "string"},
         port = {type = "integer"},
         project = {type = "string"},
@@ -66,6 +67,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         }
diff --git a/apisix/plugins/splunk-hec-logging.lua 
b/apisix/plugins/splunk-hec-logging.lua
index a7b9b2df0..ae2f73a15 100644
--- a/apisix/plugins/splunk-hec-logging.lua
+++ b/apisix/plugins/splunk-hec-logging.lua
@@ -25,6 +25,7 @@ local plugin          = require("apisix.plugin")
 local table_insert    = core.table.insert
 local table_concat    = core.table.concat
 local ipairs          = ipairs
+local pairs           = pairs
 
 
 local DEFAULT_SPLUNK_HEC_ENTRY_SOURCE = "apache-apisix-splunk-hec-logging"
@@ -67,6 +68,7 @@ local schema = {
             default = true
         },
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
     },
     encrypt_fields = {"endpoint.token"},
     required = { "endpoint" },
@@ -75,6 +77,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
@@ -105,7 +110,7 @@ end
 
 
 local function get_logger_entry(conf, ctx)
-    local entry, customized = log_util.get_log_entry(plugin_name, conf, ctx)
+    local entry, customized, extra = log_util.get_log_entry(plugin_name, conf, 
ctx)
     local splunk_entry = {
         time = ngx_now(),
         source = DEFAULT_SPLUNK_HEC_ENTRY_SOURCE,
@@ -126,6 +131,14 @@ local function get_logger_entry(conf, ctx)
             latency = entry.latency,
             upstream = entry.upstream,
         }
+        -- the fixed event above drops everything else, so add log_format_extra
+        if extra then
+            for k, v in pairs(extra) do
+                if splunk_entry.event[k] == nil then
+                    splunk_entry.event[k] = v
+                end
+            end
+        end
     else
         splunk_entry.host = core.utils.gethostname()
         splunk_entry.event = entry
diff --git a/apisix/plugins/syslog.lua b/apisix/plugins/syslog.lua
index f34542434..078d2264b 100644
--- a/apisix/plugins/syslog.lua
+++ b/apisix/plugins/syslog.lua
@@ -33,6 +33,7 @@ local schema = {
         pool_size = {type = "integer", minimum = 5, default = 5},
         tls = {type = "boolean", default = false},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
             type = "array",
@@ -61,6 +62,9 @@ local schema = batch_processor_manager:wrap_schema(schema)
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         }
diff --git a/apisix/plugins/tcp-logger.lua b/apisix/plugins/tcp-logger.lua
index d2cf97874..a52e35b6a 100644
--- a/apisix/plugins/tcp-logger.lua
+++ b/apisix/plugins/tcp-logger.lua
@@ -34,6 +34,7 @@ local schema = {
         tls_options = {type = "string"},
         timeout = {type = "integer", minimum = 1, default= 1000},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
             type = "array",
@@ -59,6 +60,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/tencent-cloud-cls.lua 
b/apisix/plugins/tencent-cloud-cls.lua
index 333742e8f..9e176b370 100644
--- a/apisix/plugins/tencent-cloud-cls.lua
+++ b/apisix/plugins/tencent-cloud-cls.lua
@@ -61,6 +61,7 @@ local schema = {
         max_resp_body_bytes = { type = "integer", minimum = 1, default = 
524288 },
         global_tag = { type = "object" },
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
     },
     encrypt_fields = {"secret_key"},
     required = { "cls_host", "cls_topic", "secret_id", "secret_key" }
@@ -70,6 +71,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/plugins/udp-logger.lua b/apisix/plugins/udp-logger.lua
index 2ea049777..ae8b62694 100644
--- a/apisix/plugins/udp-logger.lua
+++ b/apisix/plugins/udp-logger.lua
@@ -32,6 +32,7 @@ local schema = {
         port = {type = "integer", minimum = 0},
         timeout = {type = "integer", minimum = 1, default = 3},
         log_format = {type = "object"},
+        log_format_extra = {type = "object"},
         include_req_body = {type = "boolean", default = false},
         include_req_body_expr = {
             type = "array",
@@ -57,6 +58,9 @@ local schema = {
 local metadata_schema = {
     type = "object",
     properties = {
+        log_format_extra = {
+            type = "object"
+        },
         log_format = {
             type = "object"
         },
diff --git a/apisix/utils/log-util.lua b/apisix/utils/log-util.lua
index 4551b8c68..241764166 100644
--- a/apisix/utils/log-util.lua
+++ b/apisix/utils/log-util.lua
@@ -298,10 +298,21 @@ function _M.get_log_entry(plugin_name, conf, ctx)
 
     local entry
     local customized = false
+    -- resolved log_format_extra fields, surfaced to callers that rebuild a
+    -- fixed payload (e.g. google-cloud-logging, splunk) so extras aren't 
dropped
+    local extra_entry
 
     local has_meta_log_format = metadata and metadata.value.log_format
         and core.table.nkeys(metadata.value.log_format) > 0
 
+    -- conf value wins when present (even if empty), matching log_format;
+    -- only fall back to plugin metadata when conf has no log_format_extra
+    local log_format_extra = conf.log_format_extra
+    if log_format_extra == nil and metadata and 
metadata.value.log_format_extra then
+        log_format_extra = metadata.value.log_format_extra
+    end
+    local has_extra = log_format_extra and core.table.nkeys(log_format_extra) 
> 0
+
     if conf.log_format or has_meta_log_format then
         customized = true
         entry = get_custom_format_log(ctx, conf.log_format or 
metadata.value.log_format,
@@ -309,6 +320,22 @@ function _M.get_log_entry(plugin_name, conf, ctx)
     else
         if is_http then
             entry = get_full_log(ngx, conf)
+            -- enrich the default rich log with extra user-defined fields 
without
+            -- replacing it, so callers keep every default field and add their 
own
+            if has_extra then
+                -- get_custom_format_log also appends route_id/service_id; 
keep only
+                -- the user-declared keys so callers don't leak unrequested 
fields
+                local tmp = get_custom_format_log(ctx, log_format_extra,
+                                                  conf.max_req_body_bytes)
+                extra_entry = {}
+                for k in pairs(log_format_extra) do
+                    extra_entry[k] = tmp[k]
+                    -- never clobber a default field, only add new ones
+                    if entry[k] == nil then
+                        entry[k] = tmp[k]
+                    end
+                end
+            end
         else
             -- get_full_log doesn't work in stream
             core.log.error(plugin_name, "'s log_format is not set")
@@ -324,7 +351,7 @@ function _M.get_log_entry(plugin_name, conf, ctx)
     if ctx.llm_response_text then
         entry.llm_response_text = ctx.llm_response_text
     end
-    return entry, customized
+    return entry, customized, extra_entry
 end
 
 
diff --git a/docs/en/latest/apisix-variable.md 
b/docs/en/latest/apisix-variable.md
index 314d22dbf..adfb5aabd 100644
--- a/docs/en/latest/apisix-variable.md
+++ b/docs/en/latest/apisix-variable.md
@@ -50,5 +50,6 @@ additional variables.
 | redis_cmd_line      | Redis      | The content of Redis command.             
                                          |                |
 | resp_body           | core       | In the logger plugin, if some of the 
plugins support logging of response body, for example by configuring 
`include_resp_body: true`, then this variable can be used in the log format. |  
              |
 | rpc_time            | xRPC       | Time spent at the rpc request level.      
                                          |                |
+| upstream_unresolved_host | core  | The upstream host before DNS resolution 
(the configured domain or IP of the picked node). | httpbin.org |
 
 You can also register your own 
[variable](./plugin-develop.md#register-custom-variable).
diff --git a/docs/en/latest/plugins/clickhouse-logger.md 
b/docs/en/latest/plugins/clickhouse-logger.md
index 2b6074f96..5b530bc38 100644
--- a/docs/en/latest/plugins/clickhouse-logger.md
+++ b/docs/en/latest/plugins/clickhouse-logger.md
@@ -47,6 +47,7 @@ The `clickhouse-logger` Plugin pushes request and response 
logs to [ClickHouse](
 | timeout                | integer     | False    | 3                   | 
greater than 0    | Time in seconds to keep the connection alive after sending 
a request.                                                                      
                                                                                
                                                                                
    |
 | ssl_verify             | boolean     | False    | true                |      
             | If `true`, verify SSL.                                           
                                                                                
                                                                                
                                                                              |
 | log_format             | object      | False    |                     |      
             | Custom log format using key-value pairs in JSON format. Values 
can reference [APISIX variables](../apisix-variable.md) or [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) by 
prefixing with `$`. You can also configure log format on a global scale using 
[Plugin Metadata](#plugin-metadata). |
+| log_format_extra             | object      | False    |                     
|                   | Extra log fields **added on top of** the default log 
entry, keeping every default field instead of replacing them (unlike 
`log_format`). Same value syntax as `log_format`. Ignored when `log_format` is 
set. |
 | include_req_body       | boolean     | False    | false               |      
             | If `true`, include the request body in the log. Note that if the 
request body is too big to be kept in the memory, it cannot be logged due to 
NGINX's limitations.                                                            
                                                                                
 |
 | include_req_body_expr  | array       | False    |                     |      
             | An array of one or more conditions in the form of [APISIX 
expressions](https://github.com/api7/lua-resty-expr). Used when 
`include_req_body` is `true`. Request body is only logged when the expressions 
evaluate to `true`.                                                             
                      |
 | include_resp_body      | boolean     | False    | false               |      
             | If `true`, include the response body in the log.                 
                                                                                
                                                                                
                                                                              |
@@ -71,6 +72,7 @@ This Plugin supports using batch processors to aggregate and 
process entries (lo
 | Name               | Type    | Required | Default | Valid values | 
Description                                                                     
                                                                                
                                                                                
                                |
 
|--------------------|---------|----------|---------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | log_format         | object  | False    |         |              | Custom 
log format using key-value pairs in JSON format. Values can reference [APISIX 
variables](../apisix-variable.md) or [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) by 
prefixing with `$`. This configuration is global and applies to all Routes and 
Services that use the `clickhouse-logger` Plugin. |
+| log_format_extra         | object  | False    |         |              | 
Extra log fields **added on top of** the default log entry, keeping every 
default field instead of replacing them (unlike `log_format`). Same value 
syntax as `log_format`. Ignored when `log_format` is set. |
 | max_pending_entries | integer | False   |         | >= 1         | Maximum 
number of unprocessed entries allowed in the batch processor. When this limit 
is reached, new entries will be dropped until the backlog is reduced.           
                                                                                
                           |
 
 ## Examples
diff --git a/docs/en/latest/plugins/elasticsearch-logger.md 
b/docs/en/latest/plugins/elasticsearch-logger.md
index 0e4a5dfd4..58fd33119 100644
--- a/docs/en/latest/plugins/elasticsearch-logger.md
+++ b/docs/en/latest/plugins/elasticsearch-logger.md
@@ -43,6 +43,7 @@ The `elasticsearch-logger` Plugin pushes request and response 
logs in batches to
 | field         | object   | True     |                             |          
    | Elasticsearch field configuration.                          |
 | field.index   | string  | True     |                             |           
   | Elasticsearch [_index 
field](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html#mapping-index-field).
 Supports [Lua time format](https://www.lua.org/pil/22.1.html) in curly 
brackets for date-based indices (e.g., `service-{%Y-%m-%d}`) and [APISIX 
variables](../apisix-variable.md) prefixed with `$` (e.g., 
`service-$host-{%Y.%m.%d}`). |
 | log_format | object | False    |                             |              
| Custom log format as key-value pairs in JSON. Values support strings and 
nested objects (up to five levels deep; deeper fields are truncated). Within 
strings, [APISIX](../apisix-variable.md) or [NGINX 
variables](http://nginx.org/en/docs/varindex.html) can be referenced by 
prefixing with `$`. |
+| log_format_extra | object | False    |                             |         
     | Extra log fields **added on top of** the default log entry, keeping 
every default field instead of replacing them (unlike `log_format`). Same value 
syntax as `log_format`. Ignored when `log_format` is set. |
 | auth          | object   | False    |                             |          
    | Elasticsearch 
[authentication](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html)
 configuration. |
 | auth.username | string  | False    |                             |           
   | Elasticsearch 
[authentication](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html)
 username. Required if `auth` is configured. Must be provided together with 
`auth.password`. |
 | auth.password | string  | False    |                             |           
   | Elasticsearch 
[authentication](https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-up-authentication.html)
 password. Required if `auth` is configured. Must be provided together with 
`auth.username`. The secret is encrypted with AES before being stored in etcd. |
@@ -65,6 +66,7 @@ This Plugin supports using batch processors to aggregate and 
process entries (lo
 | Name | Type | Required | Default | Description |
 |------|------|----------|---------|-------------|
 | log_format | object | False |  | Log format declared as key-value pairs in 
JSON. Values support strings and nested objects (up to five levels deep; deeper 
fields are truncated). Within strings, [APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra | object | False |  | Extra log fields **added on top of** 
the default log entry, keeping every default field instead of replacing them 
(unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 | max_pending_entries | integer | False | | Maximum number of pending entries 
that can be buffered in batch processor before it starts dropping them. |
 
 ## Examples
diff --git a/docs/en/latest/plugins/google-cloud-logging.md 
b/docs/en/latest/plugins/google-cloud-logging.md
index a383521c6..0e1b47364 100644
--- a/docs/en/latest/plugins/google-cloud-logging.md
+++ b/docs/en/latest/plugins/google-cloud-logging.md
@@ -50,6 +50,7 @@ The `google-cloud-logging` Plugin pushes request and response 
logs in batches to
 | resource                | object        | False    | {"type": "global"}      
                                                                                
                                                                                
             | | Google monitored resource. See 
[MonitoredResource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource)
 for more details.                                                              
                             [...]
 | log_id                  | string        | False    | 
apisix.apache.org%2Flogs                                                        
                                                                                
                                     | | Google Cloud logging ID. See 
[LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) 
for details.                                                                    
                                                [...]
 | log_format              | object        | False    |                         
                                                                                
                                                                                
             | | Custom log format using key-value pairs in JSON format. Values 
can reference [APISIX variables](../apisix-variable.md) or [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) by 
prefixing with `$`. You can also co [...]
+| log_format_extra              | object        | False    |                   
                                                                                
                                                                                
                   | | Extra log fields **added on top of** the default log 
entry, keeping every default field instead of replacing them (unlike 
`log_format`). Same value syntax as `log_format`. Ignored when `log_format` is 
set. |
 | name                    | string        | False    | google-cloud-logging    
                                                                                
                                                                                
             | | Unique identifier of the Plugin for the batch processor. If 
you use [Prometheus](./prometheus.md) to monitor APISIX metrics, the name is 
exported in `apisix_batch_process_entries`.                                     
                    [...]
 | batch_max_size          | integer       | False    | 1000                    
                                                                                
                                                                                
             | | The number of log entries allowed in one batch. Once reached, 
the batch will be sent to the logging service. Setting this parameter to `1` 
means immediate processing.                                                     
                  [...]
 | inactive_timeout        | integer       | False    | 5                       
                                                                                
                                                                                
             | | The maximum time in seconds to wait for new logs before 
sending the batch to the logging service. The value should be smaller than 
`buffer_duration`.                                                              
                          [...]
@@ -66,6 +67,7 @@ This Plugin supports using batch processors to aggregate and 
process entries (lo
 | Name               | Type    | Required | Default | Valid values | 
Description                                                                     
                                                                                
                                                                                
                                |
 
|--------------------|---------|----------|---------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | log_format         | object  | False    |         |              | Custom 
log format using key-value pairs in JSON format. Values can reference [APISIX 
variables](../apisix-variable.md) or [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) by 
prefixing with `$`. This configuration is global and applies to all Routes and 
Services that use the `google-cloud-logging` Plugin. |
+| log_format_extra         | object  | False    |         |              | 
Extra log fields **added on top of** the default log entry, keeping every 
default field instead of replacing them (unlike `log_format`). Same value 
syntax as `log_format`. Ignored when `log_format` is set. |
 | max_pending_entries | integer | False   |         | >= 1         | Maximum 
number of unprocessed entries allowed in the batch processor. When this limit 
is reached, new entries will be dropped until the backlog is reduced.           
                                                                                
                           |
 
 ## Examples
diff --git a/docs/en/latest/plugins/http-logger.md 
b/docs/en/latest/plugins/http-logger.md
index bbbcd8093..c511f3f2f 100644
--- a/docs/en/latest/plugins/http-logger.md
+++ b/docs/en/latest/plugins/http-logger.md
@@ -43,6 +43,7 @@ The `http-logger` Plugin pushes request and response logs as 
JSON objects to HTT
 | auth_header            | string  | False    |         |                      
| Authorization headers, if required by the HTTP(S) server.                     
                                                                                
                                                                                
                              |
 | timeout                | integer | False    | 3       | greater than 0       
| Time to keep the connection alive after sending a request.                    
                                                                                
                                                                                
                               |
 | log_format             | object  | False    |         |                      
| Custom log format using key-value pairs in JSON format. Values can reference 
[NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html). 
You can also configure log format on a global scale using the [plugin 
metadata](../terminology/plugin-metadata.md), which configures the log format 
for all `http-logger` Plugin instances. If the log format configured on the 
individual Plugin instance differ [...]
+| log_format_extra             | object  | False    |         |                
      | Extra log fields **added on top of** the default log entry, keeping 
every default field instead of replacing them (unlike `log_format`). Same value 
syntax as `log_format`. Ignored when `log_format` is set. |
 | include_req_body       | boolean | False    | false   |                      
| If true, include the request body in the log. Note that if the request body 
is too big to be kept in the memory, it cannot be logged due to NGINX's 
limitations.                                                                    
                                         |
 | include_req_body_expr  | array   | False    |         |                      
| An array of one or more conditions in the form of 
[lua-resty-expr](https://github.com/api7/lua-resty-expr) expressions. Used when 
`include_req_body` is true. Request body is only logged when the expressions 
configured here evaluate to true.                              |
 | include_resp_body      | boolean | False    | false   |                      
| If true, include the response body in the log.                                
                                                                                
                                                                                
                               |
@@ -65,6 +66,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name                | Type    | Required | Description                       
                                                                                
                                                                                
              |
 
|---------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | log_format          | object  | False    | Custom log format using key-value 
pairs in JSON format. Values can reference [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html).           
                                            |
+| log_format_extra          | object  | False    | Extra log fields **added on 
top of** the default log entry, keeping every default field instead of 
replacing them (unlike `log_format`). Same value syntax as `log_format`. 
Ignored when `log_format` is set. |
 | max_pending_entries | integer | False    | Maximum number of unprocessed 
entries allowed in the batch processor. When this limit is reached, new entries 
will be dropped until the backlog is reduced. Available in APISIX from version 
3.15.0.            |
 
 :::info IMPORTANT
diff --git a/docs/en/latest/plugins/kafka-logger.md 
b/docs/en/latest/plugins/kafka-logger.md
index defd99397..f57081183 100644
--- a/docs/en/latest/plugins/kafka-logger.md
+++ b/docs/en/latest/plugins/kafka-logger.md
@@ -57,6 +57,7 @@ It might take some time to receive the log data. It will be 
automatically sent a
 | name                             | string  | False    | "kafka logger" |     
                                              | Unique identifier for the batch 
processor. If you use Prometheus to monitor APISIX metrics, the name is 
exported in `apisix_batch_process_entries`.                                     
                                                                                
                                                                         |
 | meta_format                      | enum    | False    | "default"      | 
["default","origin"]                              | Format to collect the 
request information. Setting to `default` collects the information in JSON 
format and `origin` collects the information with the original HTTP request. 
See [examples](#meta_format-example) below.                                     
                                                                                
    |
 | log_format                       | object  | False    |                |     
                                              | Log format declared as 
key-value pairs in JSON. Values support strings and nested objects (up to five 
levels deep; deeper fields are truncated). Within strings, 
[APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`.                                           |
+| log_format_extra                       | object  | False    |                
|                                                   | Extra log fields **added 
on top of** the default log entry, keeping every default field instead of 
replacing them (unlike `log_format`). Same value syntax as `log_format`. 
Ignored when `log_format` is set. |
 | include_req_body                 | boolean | False    | false          | 
[false, true]                                     | When set to `true` includes 
the request body in the log. If the request body is too big to be kept in the 
memory, it can't be logged due to NGINX's limitations.                          
                                                                                
                                                                       |
 | include_req_body_expr            | array   | False    |                |     
                                              | Filter for when the 
`include_req_body` attribute is set to `true`. Request body is only logged when 
the expression set here evaluates to `true`. See 
[lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.              
                                                                                
                            |
 | max_req_body_bytes               | integer | False    | 524288         | >=1 
                                              | Maximum request body size in 
bytes to push to Kafka. If the size exceeds the configured value, the body will 
be truncated before being pushed.                                               
                                                                                
                                                                    |
@@ -144,6 +145,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name                | Type    | Required | Default | Description             
                                                                                
                                                                                
                                                                |
 | ------------------- | ------- | -------- | ------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
 | log_format          | object  | False    |         | Log format declared as 
key-value pairs in JSON. Values support strings and nested objects (up to five 
levels deep; deeper fields are truncated). Within strings, 
[APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra          | object  | False    |         | Extra log fields 
**added on top of** the default log entry, keeping every default field instead 
of replacing them (unlike `log_format`). Same value syntax as `log_format`. 
Ignored when `log_format` is set. |
 | max_pending_entries | integer | False    |         | Maximum number of 
pending entries that can be buffered in the batch processor before it starts 
dropping them.                                                                  
                                                                         |
 
 :::info IMPORTANT
diff --git a/docs/en/latest/plugins/loggly.md b/docs/en/latest/plugins/loggly.md
index 4d6ccb737..e672f9fcb 100644
--- a/docs/en/latest/plugins/loggly.md
+++ b/docs/en/latest/plugins/loggly.md
@@ -44,6 +44,7 @@ When the maximum batch size is exceeded, the data in the 
queue is pushed to Logg
 | severity_map           | object        | False    | nil     | A way to map 
upstream HTTP response codes to Syslog severity. Key-value pairs where keys are 
the HTTP response codes and the values are the Syslog severity levels. For 
example `{"410": "CRIT"}`.                             |
 | tags                   | array         | False    |         | Metadata to be 
included with any event log to aid in segmentation and filtering.               
                                                                                
                                                |
 | log_format             | object        | False    | {"host": "$host", 
"@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | Log format 
declared as key-value pairs in JSON. Values support strings and nested objects 
(up to five levels deep; deeper fields are truncated). Within strings, 
[APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra             | object        | False    |                    
                                                          | Extra log fields 
**added on top of** the default log entry, keeping every default field instead 
of replacing them (unlike `log_format`). Same value syntax as `log_format`. 
Ignored when `log_format` is set. |
 | include_req_body       | boolean       | False    | false   | When set to 
`true` includes the request body in the log. If the request body is too big to 
be kept in the memory, it can't be logged due to Nginx's limitations.           
                                                    |
 | include_req_body_expr  | array         | False    |         | Filter for 
when the `include_req_body` attribute is set to `true`. Request body is only 
logged when the expression set here evaluates to `true`. See 
[lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.        |
 | max_req_body_bytes | integer | False | 524288 | Request bodies within this 
size will be logged, if the size exceeds the configured value it will be 
truncated before logging. |
@@ -72,6 +73,7 @@ You can also configure the Plugin through Plugin metadata. 
The following configu
 | timeout    | integer | False    | 5000                 |                     
           | Loggly send data request timeout in milliseconds.                  
                                                                                
                                                                            |
 | protocol   | string  | False    | "syslog"             | [ "syslog" , 
"http", "https" ] | Protocol in which the logs are sent to Loggly.              
                                                                                
                                                                                
   |
 | log_format | object  | False    | nil                  |                     
           | Log format declared as key-value pairs in JSON. Values support 
strings and nested objects (up to five levels deep; deeper fields are 
truncated). Within strings, [APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra | object  | False    |                      |               
                 | Extra log fields **added on top of** the default log entry, 
keeping every default field instead of replacing them (unlike `log_format`). 
Same value syntax as `log_format`. Ignored when `log_format` is set. |
 
 We support 
[Syslog](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/streaming-syslog-without-using-files.htm),
 
[HTTP/S](https://documentation.solarwinds.com/en/success_center/loggly/content/admin/http-bulk-endpoint.htm)
 (bulk endpoint) protocols to send log events to Loggly. By default, in APISIX 
side, the protocol is set to "syslog". It lets you send RFC5424 compliant 
syslog events with some fine-grained control (log severity mapping based on 
upstream HTTP re [...]
 
diff --git a/docs/en/latest/plugins/loki-logger.md 
b/docs/en/latest/plugins/loki-logger.md
index 07d77f857..6b7bd6c93 100644
--- a/docs/en/latest/plugins/loki-logger.md
+++ b/docs/en/latest/plugins/loki-logger.md
@@ -52,7 +52,8 @@ When enabled, the Plugin will serialize the request context 
information to [JSON
 | keepalive         | boolean       | False    | true |  | If true, keep the 
connection alive for multiple requests. |
 | keepalive_timeout | integer       | False    | 60000 | >=1000 | Keepalive 
timeout in milliseconds.  |
 | keepalive_pool    | integer       | False    | 5       | >=1 | Maximum 
number of connections in the connection pool.  |
-| log_format | object | False    |          | | Custom log format as key-value 
pairs in JSON. Values support strings and nested objects (up to five levels 
deep; deeper fields are truncated). Within strings, [APISIX 
variables](../apisix-variable.md) and [NGINX 
variables](http://nginx.org/en/docs/varindex.html) can be referenced by 
prefixing with `$`. |
+| log_format | object | False    |          | | Custom log format as key-value 
pairs in JSON. Setting this **replaces** the default log entry with a flat 
custom format. Values support strings and nested objects (up to five levels 
deep; deeper fields are truncated). Within strings, [APISIX 
variables](../apisix-variable.md) and [NGINX 
variables](http://nginx.org/en/docs/varindex.html) can be referenced by 
prefixing with `$`. |
+| log_format_extra | object | False    |          | | Extra log fields **added 
on top of** the default log entry, keeping every default field instead of 
replacing them (unlike `log_format`). Same value syntax as `log_format`. 
Ignored when `log_format` is set. |
 | name | string | False    | loki-logger | | Unique identifier of the Plugin 
for the batch processor. If you use [Prometheus](./prometheus.md) to monitor 
APISIX metrics, the name is exported in `apisix_batch_process_entries`. |
 | include_req_body       | boolean | False    | false | | If true, include the 
request body in the log. Note that if the request body is too big to be kept in 
the memory, it can not be logged due to NGINX's limitations. |
 | include_req_body_expr  | array[array]   | False    |  | | An array of one or 
more conditions in the form of 
[lua-resty-expr](https://github.com/api7/lua-resty-expr). Used when the 
`include_req_body` is true. Request body would only be logged when the 
expressions configured here evaluate to true. |
@@ -69,7 +70,8 @@ You can also configure log format on a global scale using the 
[Plugin Metadata](
 
 | Name | Type | Required | Default | Valid values | Description |
 |------|------|----------|---------|--------------|-------------|
-| log_format | object | False |  |  | Custom log format as key-value pairs in 
JSON. Values support strings and nested objects (up to five levels deep; deeper 
fields are truncated). Within strings, [APISIX 
variables](../apisix-variable.md) and [NGINX 
variables](http://nginx.org/en/docs/varindex.html) can be referenced by 
prefixing with `$`. |
+| log_format | object | False |  |  | Custom log format as key-value pairs in 
JSON. Setting this **replaces** the default log entry with a flat custom 
format. Values support strings and nested objects (up to five levels deep; 
deeper fields are truncated). Within strings, [APISIX 
variables](../apisix-variable.md) and [NGINX 
variables](http://nginx.org/en/docs/varindex.html) can be referenced by 
prefixing with `$`. |
+| log_format_extra | object | False |  |  | Extra log fields **added on top 
of** the default log entry, keeping every default field instead of replacing 
them (unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 | max_pending_entries | integer | False | |  | Maximum number of pending 
entries that can be buffered in batch processor before it starts dropping them. 
|
 
 ## Examples
diff --git a/docs/en/latest/plugins/rocketmq-logger.md 
b/docs/en/latest/plugins/rocketmq-logger.md
index 96a6db7b4..2b3d2a927 100644
--- a/docs/en/latest/plugins/rocketmq-logger.md
+++ b/docs/en/latest/plugins/rocketmq-logger.md
@@ -43,6 +43,7 @@ The `rocketmq-logger` Plugin pushes request and response logs 
as JSON objects to
 | key                    | string  | False    |                    |           
            | Key of the message.                                               
                                                                                
                                                                                
                                                                             |
 | tag                    | string  | False    |                    |           
            | Tag of the message.                                               
                                                                                
                                                                                
                                                                             |
 | log_format             | object  | False    |                    |           
            | Custom log format using key-value pairs in JSON format. Values 
can reference [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html). You can 
also configure log format on a global scale using the [plugin 
metadata](../terminology/plugin-metadata.md), which configures the log format 
for all `rocketmq-logger` Plugin instances. If the log format configured on the 
individual Plugin [...]
+| log_format_extra             | object  | False    |                    |     
                  | Extra log fields **added on top of** the default log entry, 
keeping every default field instead of replacing them (unlike `log_format`). 
Same value syntax as `log_format`. Ignored when `log_format` is set. |
 | timeout                | integer | False    | 3                  |           
            | Timeout for the upstream to send data.                            
                                                                                
                                                                                
                                                                             |
 | use_tls                | boolean | False    | false              |           
            | If true, enable TLS encryption for the connection.                
                                                                                
                                                                                
                                                                             |
 | access_key             | string  | False    |                    |           
            | Access key for ACL. Setting to an empty string will disable the 
ACL.                                                                            
                                                                                
                                                                               |
@@ -70,6 +71,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name                | Type    | Required | Description                       
                                                                                
                                                                                
              |
 
|---------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | log_format          | object  | False    | Custom log format using key-value 
pairs in JSON format. Values can reference [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html).           
                                            |
+| log_format_extra          | object  | False    | Extra log fields **added on 
top of** the default log entry, keeping every default field instead of 
replacing them (unlike `log_format`). Same value syntax as `log_format`. 
Ignored when `log_format` is set. |
 | max_pending_entries | integer | False    | Maximum number of unprocessed 
entries allowed in the batch processor. When this limit is reached, new entries 
will be dropped until the backlog is reduced. Available in APISIX from version 
3.15.0.            |
 
 :::info IMPORTANT
diff --git a/docs/en/latest/plugins/skywalking-logger.md 
b/docs/en/latest/plugins/skywalking-logger.md
index 5b036b662..d61589e7f 100644
--- a/docs/en/latest/plugins/skywalking-logger.md
+++ b/docs/en/latest/plugins/skywalking-logger.md
@@ -46,6 +46,7 @@ If there is an existing tracing context, it sets up the 
trace-log correlation au
 | service_name          | string  | False    | "APISIX"               |        
       | Service name for the SkyWalking reporter.                              
                                      |
 | service_instance_name | string  | False    | "APISIX Instance Name" |        
       | Service instance name for the SkyWalking reporter. Set it to 
`$hostname` to directly get the local hostname. |
 | log_format | object | False    |                             | Custom log 
format as key-value pairs in JSON. Values support strings and nested objects 
(up to five levels deep; deeper fields are truncated). Within strings, 
[APISIX](../apisix-variable.md) or [NGINX 
variables](http://nginx.org/en/docs/varindex.html) can be referenced by 
prefixing with `$`. |
+| log_format_extra | object | False    |                             | Extra 
log fields **added on top of** the default log entry, keeping every default 
field instead of replacing them (unlike `log_format`). Same value syntax as 
`log_format`. Ignored when `log_format` is set. |
 | timeout               | integer | False    | 3                      | 
[1,...]       | Time to keep the connection alive for after sending a request.  
                                             |
 | name                  | string  | False    | "skywalking logger"    |        
       | Unique identifier to identify the logger. If you use Prometheus to 
monitor APISIX metrics, the name is exported in `apisix_batch_process_entries`. 
                                                                   |
 | include_req_body       | boolean       | False    | false   |  If true, 
include the request body in the log. Note that if the request body is too big 
to be kept in the memory, it can not be logged due to NGINX's limitations.      
 |
@@ -64,6 +65,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name       | Type   | Required | Default                                     
                                  | Description                                 
                                                                                
                                                                                
                                            |
 | ---------- | ------ | -------- | 
----------------------------------------------------------------------------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
 | log_format | object | False    |  | Custom log format as key-value pairs in 
JSON. Values support strings and nested objects (up to five levels deep; deeper 
fields are truncated). Within strings, [APISIX](../apisix-variable.md) or 
[NGINX variables](http://nginx.org/en/docs/varindex.html) can be referenced by 
prefixing with `$`. |
+| log_format_extra | object | False    |  | Extra log fields **added on top 
of** the default log entry, keeping every default field instead of replacing 
them (unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 | max_pending_entries | integer | False | | Maximum number of pending entries 
that can be buffered in batch processor before it starts dropping them. |
 
 ## Examples
diff --git a/docs/en/latest/plugins/sls-logger.md 
b/docs/en/latest/plugins/sls-logger.md
index c13fc2cb0..b5d8ab1d4 100644
--- a/docs/en/latest/plugins/sls-logger.md
+++ b/docs/en/latest/plugins/sls-logger.md
@@ -41,6 +41,7 @@ It might take some time to receive the log data. It will be 
automatically sent a
 | port              | True     | Target upstream port. Defaults to `10009`.    
                                                                                
                                                                                
                                  |
 | timeout           | False    | Timeout for the upstream to send data.        
                                                                                
                                                                                
                                  |
 | log_format       | False    | Log format declared as key-value pairs in 
JSON. Values support strings and nested objects (up to five levels deep; deeper 
fields are truncated). Within strings, [APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra       | False    | Extra log fields **added on top of** the 
default log entry, keeping every default field instead of replacing them 
(unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 | project           | True     | Project name in Alibaba Cloud log service. 
Create SLS before using this Plugin.                                            
                                                                                
                                         |
 | logstore          | True     | logstore name in Ali Cloud log service. 
Create SLS before using this Plugin.                                            
                                                                                
                                        |
 | access_key_id     | True     | AccessKey ID in Alibaba Cloud. See 
[Authorization](https://www.alibabacloud.com/help/en/log-service/latest/create-a-ram-user-and-authorize-the-ram-user-to-access-log-service)
 for more details.                                                              
       |
@@ -91,6 +92,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name       | Type   | Required | Default                                     
                                  | Description                                 
                                                                                
                                                                                
                                            |
 | ---------- | ------ | -------- | 
----------------------------------------------------------------------------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
 | log_format | object | False    |  | Log format declared as key-value pairs 
in JSON. Values support strings and nested objects (up to five levels deep; 
deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) 
or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced 
by prefixing with `$`. |
+| log_format_extra | object | False    |  | Extra log fields **added on top 
of** the default log entry, keeping every default field instead of replacing 
them (unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 
 :::info IMPORTANT
 
diff --git a/docs/en/latest/plugins/splunk-hec-logging.md 
b/docs/en/latest/plugins/splunk-hec-logging.md
index 44bf51be8..6ebab57ce 100644
--- a/docs/en/latest/plugins/splunk-hec-logging.md
+++ b/docs/en/latest/plugins/splunk-hec-logging.md
@@ -48,6 +48,7 @@ The `splunk-hec-logging` Plugin serializes request and 
response context informat
 | endpoint.keepalive_timeout | integer | False    | 60000            | >= 1000 
       | Keepalive timeout in milliseconds.                                     
                                                                                
                                                                                
                                                                              |
 | ssl_verify                 | boolean | False    | true             |         
       | If `true`, enables SSL verification as per [OpenResty 
docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake).       
                                                                                
                                                                                
               |
 | log_format                 | object  | False    |                  |         
       | Custom log format using key-value pairs in JSON format. Values can 
reference [APISIX variables](../apisix-variable.md) or [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) by 
prefixing with `$`. You can also configure log format on a global scale using 
[Plugin Metadata](#plugin-metadata). |
+| log_format_extra                 | object  | False    |                  |   
             | Extra log fields **added on top of** the default log entry, 
keeping every default field instead of replacing them (unlike `log_format`). 
Same value syntax as `log_format`. Ignored when `log_format` is set. |
 | name                       | string  | False    | splunk-hec-logging |       
       | Unique identifier of the Plugin for the batch processor.               
                                                                                
                                                                                
                                                                              |
 | batch_max_size             | integer | False    | 1000             | greater 
than 0 | The number of log entries allowed in one batch. Once reached, the 
batch will be sent to Splunk HEC. Setting this parameter to `1` means immediate 
processing.                                                                     
                                                                                
   |
 | inactive_timeout           | integer | False    | 5                | greater 
than 0 | The maximum time in seconds to wait for new logs before sending the 
batch to the logging service. The value should be smaller than 
`buffer_duration`.                                                              
                                                                                
                  |
@@ -62,6 +63,7 @@ This Plugin supports using batch processors to aggregate and 
process entries (lo
 | Name               | Type    | Required | Default | Valid values | 
Description                                                                     
                                                                                
                                                                                
                                |
 
|--------------------|---------|----------|---------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | log_format         | object  | False    |         |              | Custom 
log format using key-value pairs in JSON format. Values can reference [APISIX 
variables](../apisix-variable.md) or [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) by 
prefixing with `$`. This configuration is global and applies to all Routes and 
Services that use the `splunk-hec-logging` Plugin. |
+| log_format_extra         | object  | False    |         |              | 
Extra log fields **added on top of** the default log entry, keeping every 
default field instead of replacing them (unlike `log_format`). Same value 
syntax as `log_format`. Ignored when `log_format` is set. |
 | max_pending_entries | integer | False   |         | >= 1         | Maximum 
number of unprocessed entries allowed in the batch processor. When this limit 
is reached, new entries will be dropped until the backlog is reduced.           
                                                                                
                           |
 
 ## Examples
diff --git a/docs/en/latest/plugins/syslog.md b/docs/en/latest/plugins/syslog.md
index 13eb01690..787c695d1 100644
--- a/docs/en/latest/plugins/syslog.md
+++ b/docs/en/latest/plugins/syslog.md
@@ -47,6 +47,7 @@ The `syslog` Plugin pushes request and response logs as JSON 
objects to syslog s
 | sock_type              | string  | False    | `tcp`        | `tcp` or `udp`  
      | Transport layer protocol to use.                                        
                                                                                
                                                                  |
 | pool_size              | integer | False    | 5            | greater than or 
equal to 5 | Keep-alive pool size used by `sock:keepalive`.                     
                                                                                
                                                                  |
 | log_format             | object  | False    |              |                 
      | Custom log format using key-value pairs in JSON format. Values can 
reference [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html). You can 
also configure log format on a global scale using the [plugin 
metadata](../terminology/plugin-metadata.md), which configures the log format 
for all `syslog` Plugin instances. If the log format configured on the 
individual Plugin instance diffe [...]
+| log_format_extra             | object  | False    |              |           
            | Extra log fields **added on top of** the default log entry, 
keeping every default field instead of replacing them (unlike `log_format`). 
Same value syntax as `log_format`. Ignored when `log_format` is set. |
 | include_req_body       | boolean | False    | false        |                 
      | If true, include the request body in the log. Note that if the request 
body is too big to be kept in the memory, it cannot be logged due to NGINX's 
limitations.                                                          |
 | include_req_body_expr  | array   | False    |              |                 
      | An array of one or more conditions in the form of 
[lua-resty-expr](https://github.com/api7/lua-resty-expr) expressions. Used when 
`include_req_body` is true. Request body is only logged when the expressions 
configured here evaluate to true. |
 | include_resp_body      | boolean | False    | false        |                 
      | If true, include the response body in the log.                          
                                                                                
                                                                  |
@@ -73,6 +74,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name       | Type   | Required | Description                                 
                                                                                
                                             |
 
|------------|--------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | log_format | object | False    | Custom log format using key-value pairs in 
JSON format. Values can reference [NGINX 
variables](https://nginx.org/en/docs/http/ngx_http_core_module.html).           
     |
+| log_format_extra | object | False    | Extra log fields **added on top of** 
the default log entry, keeping every default field instead of replacing them 
(unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 
 :::info IMPORTANT
 
diff --git a/docs/en/latest/plugins/tcp-logger.md 
b/docs/en/latest/plugins/tcp-logger.md
index 582610994..ac284d414 100644
--- a/docs/en/latest/plugins/tcp-logger.md
+++ b/docs/en/latest/plugins/tcp-logger.md
@@ -44,6 +44,7 @@ This plugin also allows to push logs as a batch to your 
external TCP server. It
 | port             | integer | True     |         | [0,...]      | Target 
upstream port.                                    |
 | timeout          | integer | False    | 1000    | [1,...]      | Timeout for 
the upstream to send data.                   |
 | log_format       | object  | False    |  |              | Log format 
declared as key-value pairs in JSON. Values support strings and nested objects 
(up to five levels deep; deeper fields are truncated). Within strings, 
[APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra       | object  | False    |  |              | Extra log 
fields **added on top of** the default log entry, keeping every default field 
instead of replacing them (unlike `log_format`). Same value syntax as 
`log_format`. Ignored when `log_format` is set. |
 | tls              | boolean | False    | false   |              | When set to 
`true` performs SSL verification.            |
 | tls_options      | string  | False    |         |              | TLS 
options.                                             |
 | include_req_body | boolean | False    | false   | [false, true] | When set 
to `true` includes the request body in the log. |
@@ -102,6 +103,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name       | Type   | Required | Default                                     
                                  | Description                                 
                                                                                
                                                                                
                                            |
 | ---------- | ------ | -------- | 
----------------------------------------------------------------------------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
 | log_format | object | False    |  | Log format declared as key-value pairs 
in JSON. Values support strings and nested objects (up to five levels deep; 
deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) 
or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced 
by prefixing with `$`. |
+| log_format_extra | object | False    |  | Extra log fields **added on top 
of** the default log entry, keeping every default field instead of replacing 
them (unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 | max_pending_entries | integer | False | | Maximum number of pending entries 
that can be buffered in batch processor before it starts dropping them. |
 
 :::info IMPORTANT
diff --git a/docs/en/latest/plugins/tencent-cloud-cls.md 
b/docs/en/latest/plugins/tencent-cloud-cls.md
index 5d667efa1..980072d3a 100644
--- a/docs/en/latest/plugins/tencent-cloud-cls.md
+++ b/docs/en/latest/plugins/tencent-cloud-cls.md
@@ -50,6 +50,7 @@ The `tencent-cloud-cls` Plugin uses [TencentCloud 
CLS](https://cloud.tencent.com
 | max_resp_body_bytes | integer | False | 524288 | >=1 | Response bodies 
within this size will be logged, if the size exceeds the configured value it 
will be truncated before logging. |
 | global_tag        | object  | No       |         |               | kv pairs 
in JSON,send with each log.                                                     
                                                                        |
 | log_format       | object  | No       |         |               | Log format 
declared as key-value pairs in JSON. Values support strings and nested objects 
(up to five levels deep; deeper fields are truncated). Within strings, 
[APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra       | object  | No       |         |               | 
Extra log fields **added on top of** the default log entry, keeping every 
default field instead of replacing them (unlike `log_format`). Same value 
syntax as `log_format`. Ignored when `log_format` is set. |
 
 NOTE: `encrypt_fields = {"secret_key"}` is also defined in the schema, which 
means that the field will be stored encrypted in etcd. See [encrypted storage 
fields](../plugin-develop.md#encrypted-storage-fields).
 
@@ -102,6 +103,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name       | Type   | Required | Default                                     
                                  | Description                                 
                                                                                
                                                                                
                                            |
 | ---------- | ------ | -------- | 
----------------------------------------------------------------------------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
 | log_format | object | False    |   | Log format declared as key-value pairs 
in JSON. Values support strings and nested objects (up to five levels deep; 
deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) 
or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced 
by prefixing with `$`. |
+| log_format_extra | object | False    |   | Extra log fields **added on top 
of** the default log entry, keeping every default field instead of replacing 
them (unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 | max_pending_entries | integer | False | | Maximum number of pending entries 
that can be buffered in batch processor before it starts dropping them. |
 
 :::info IMPORTANT
diff --git a/docs/en/latest/plugins/udp-logger.md 
b/docs/en/latest/plugins/udp-logger.md
index 1d81c11f7..9eca52e93 100644
--- a/docs/en/latest/plugins/udp-logger.md
+++ b/docs/en/latest/plugins/udp-logger.md
@@ -43,6 +43,7 @@ This plugin also allows to push logs as a batch to your 
external UDP server. It
 | port             | integer | True     |              | [0,...]      | Target 
upstream port.                                    |
 | timeout          | integer | False    | 3            | [1,...]      | 
Timeout for the upstream to send data.                   |
 | log_format       | object  | False    |  |              | Log format 
declared as key-value pairs in JSON. Values support strings and nested objects 
(up to five levels deep; deeper fields are truncated). Within strings, 
[APISIX](../apisix-variable.md) or 
[NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced by 
prefixing with `$`. |
+| log_format_extra       | object  | False    |  |              | Extra log 
fields **added on top of** the default log entry, keeping every default field 
instead of replacing them (unlike `log_format`). Same value syntax as 
`log_format`. Ignored when `log_format` is set. |
 | name             | string  | False    | "udp logger" |              | Unique 
identifier for the batch processor. If you use Prometheus to monitor APISIX 
metrics, the name is exported in `apisix_batch_process_entries`. processor.     
          |
 | include_req_body | boolean | False    | false        | [false, true] | When 
set to `true` includes the request body in the log. |
 | include_req_body_expr  | array   | No       |         |               | 
Filter for when the `include_req_body` attribute is set to `true`. Request body 
is only logged when the expression set here evaluates to `true`. See 
[lua-resty-expr](https://github.com/api7/lua-resty-expr) for more.              
                                                                                
                            |
@@ -100,6 +101,7 @@ You can also set the format of the logs by configuring the 
Plugin metadata. The
 | Name       | Type   | Required | Default                                     
                                  | Description                                 
                                                                                
                                                                                
                                            |
 | ---------- | ------ | -------- | 
----------------------------------------------------------------------------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
 | log_format | object | False    |  | Log format declared as key-value pairs 
in JSON. Values support strings and nested objects (up to five levels deep; 
deeper fields are truncated). Within strings, [APISIX](../apisix-variable.md) 
or [NGINX](http://nginx.org/en/docs/varindex.html) variables can be referenced 
by prefixing with `$`. |
+| log_format_extra | object | False    |  | Extra log fields **added on top 
of** the default log entry, keeping every default field instead of replacing 
them (unlike `log_format`). Same value syntax as `log_format`. Ignored when 
`log_format` is set. |
 | max_pending_entries | integer | False | | Maximum number of pending entries 
that can be buffered in batch processor before it starts dropping them. |
 
 :::info IMPORTANT
diff --git a/t/APISIX.pm b/t/APISIX.pm
index 9c04f47ff..e0b86560b 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -718,7 +718,7 @@ _EOC_
         require("apisix").http_exit_worker()
     }
 
-    log_format main escape=default '\$remote_addr - \$remote_user 
[\$time_local] \$http_host "\$request_line" \$status \$body_bytes_sent 
\$request_time "\$http_referer" "\$http_user_agent" \$upstream_addr 
\$upstream_status \$apisix_upstream_response_time 
"\$upstream_scheme://\$upstream_host\$upstream_uri" \$request_llm_model 
\$llm_model \$llm_time_to_first_token \$llm_prompt_tokens 
\$llm_completion_tokens \$llm_total_tokens \$llm_stream \$llm_has_tool_calls 
\$llm_tool_count \$llm_end_use [...]
+    log_format main escape=default '\$remote_addr - \$remote_user 
[\$time_local] \$http_host "\$request_line" \$status \$body_bytes_sent 
\$request_time "\$http_referer" "\$http_user_agent" \$upstream_addr 
\$upstream_status \$apisix_upstream_response_time 
"\$upstream_scheme://\$upstream_host\$upstream_uri" \$request_llm_model 
\$llm_model \$llm_time_to_first_token \$llm_prompt_tokens 
\$llm_completion_tokens \$llm_total_tokens \$llm_stream \$llm_has_tool_calls 
\$llm_tool_count \$llm_end_use [...]
 
     # fake server, only for test
     server {
@@ -896,6 +896,7 @@ _EOC_
 
             set \$upstream_scheme             'http';
             set \$upstream_host               \$http_host;
+            set \$upstream_unresolved_host    '';
             set \$upstream_uri                '';
             set \$request_line                '';
             set \$ctx_ref                     '';
diff --git a/t/core/ctx.t b/t/core/ctx.t
index d49d87699..af4ab2408 100644
--- a/t/core/ctx.t
+++ b/t/core/ctx.t
@@ -915,3 +915,56 @@ GET /hello
 hello world
 --- error_log eval
 qr/request log: 
\{"route_id":"1","route_name":"my_route","service_id":"1","service_name":"my_service"\}/
+
+
+
+=== TEST 36: upstream_unresolved_host is available in the nginx access log
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/hello",
+                    "upstream": {
+                        "nodes": {
+                            "localhost:1980": 1
+                        },
+                        "type": "roundrobin"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            -- a real proxied request so the access log line gets written
+            local http = require("resty.http")
+            local httpc = http.new()
+            local res, err = httpc:request_uri(
+                "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello")
+            if not res or res.status ~= 200 then
+                ngx.say("request failed: ", err or res.status)
+                return
+            end
+            -- let the log phase flush the access log line
+            ngx.sleep(0.1)
+
+            local fd = assert(io.open(ngx.config.prefix() .. 
"logs/access.log", "r"))
+            local content = fd:read("*a")
+            fd:close()
+            -- the configured domain shows up before DNS resolution
+            if content:find("unresolved_host=localhost", 1, true) then
+                ngx.say("found")
+            else
+                ngx.say("not found")
+            end
+        }
+    }
+--- request
+GET /t
+--- response_body
+found
diff --git a/t/plugin/file-logger.t b/t/plugin/file-logger.t
index 127578e34..2213213ba 100644
--- a/t/plugin/file-logger.t
+++ b/t/plugin/file-logger.t
@@ -572,3 +572,420 @@ passed
     }
 --- response_body
 write file log success
+
+
+
+=== TEST 15: log_format_extra enriches the default log without replacing it
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            -- additive log format via plugin metadata: keep the rich default 
and
+            -- add the pre-DNS upstream host on top
+            local code, body = t('/apisix/admin/plugin_metadata/file-logger',
+                ngx.HTTP_PUT,
+                [[{
+                    "path": "file-logger-extra.log",
+                    "log_format_extra": {
+                        "upstream_host": "$upstream_unresolved_host"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "file-logger": {}
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1982": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 16: default fields stay and the extra field is added
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code = t("/hello", ngx.HTTP_GET)
+            local fd, err = io.open("file-logger-extra.log", 'r')
+            if not fd then
+                core.log.error("failed to open file: file-logger-extra.log, 
error info: ", err)
+                return
+            end
+            local msg = fd:read()
+            fd:close()
+
+            local new_msg = core.json.decode(msg)
+            -- the extra field is present
+            if new_msg.upstream_host == '127.0.0.1' and
+               -- and the rich default fields are still there
+               type(new_msg.request) == "table" and
+               new_msg.request.method == 'GET' and
+               type(new_msg.response) == "table" and
+               new_msg.response.status == 200 and
+               type(new_msg.server) == "table" and
+               new_msg.server.version and
+               new_msg.route_id == '1'
+            then
+                ngx.status = code
+                ngx.say("enrich log format success")
+            else
+                ngx.say("enrich log format failed: " .. msg)
+            end
+        }
+    }
+--- response_body
+enrich log format success
+
+
+
+=== TEST 17: log_format_extra logs the pre-DNS host for a domain upstream
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/plugin_metadata/file-logger',
+                ngx.HTTP_PUT,
+                [[{
+                    "path": "file-logger-domain.log",
+                    "log_format_extra": {
+                        "upstream_host": "$upstream_unresolved_host"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "file-logger": {}
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "localhost:1982": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 18: extra field keeps the domain while the default upstream is 
resolved
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code = t("/hello", ngx.HTTP_GET)
+            local fd, err = io.open("file-logger-domain.log", 'r')
+            if not fd then
+                core.log.error("failed to open file: file-logger-domain.log, 
error info: ", err)
+                return
+            end
+            local msg = fd:read()
+            fd:close()
+
+            local new_msg = core.json.decode(msg)
+            -- the extra field carries the configured hostname, before DNS
+            if new_msg.upstream_host == 'localhost' and
+               -- while the default upstream field is the resolved ip:port
+               new_msg.upstream == '127.0.0.1:1982' and
+               -- and the rich default fields are still there
+               type(new_msg.request) == "table" and
+               new_msg.request.method == 'GET' and
+               type(new_msg.response) == "table" and
+               new_msg.response.status == 200 and
+               type(new_msg.server) == "table" and
+               new_msg.server.version and
+               new_msg.route_id == '1'
+            then
+                ngx.status = code
+                ngx.say("enrich log format success")
+            else
+                ngx.say("enrich log format failed: " .. msg)
+            end
+        }
+    }
+--- response_body
+enrich log format success
+
+
+
+=== TEST 19: log_format wins and log_format_extra is ignored when both are set
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "file-logger": {
+                                "path": "file-logger-precedence.log",
+                                "log_format": {
+                                    "msg": "precedence test"
+                                },
+                                "log_format_extra": {
+                                    "upstream_host": 
"$upstream_unresolved_host"
+                                }
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1982": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 20: extra field absent and the default entry is replaced by log_format
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code = t("/hello", ngx.HTTP_GET)
+            local fd, err = io.open("file-logger-precedence.log", 'r')
+            if not fd then
+                core.log.error("failed to open file: 
file-logger-precedence.log, error info: ", err)
+                return
+            end
+            local msg = fd:read()
+            fd:close()
+
+            local new_msg = core.json.decode(msg)
+            -- log_format replaced the default entry, extra was ignored
+            if new_msg.msg == 'precedence test' and
+               new_msg.upstream_host == nil and
+               new_msg.request == nil and
+               new_msg.response == nil
+            then
+                ngx.status = code
+                ngx.say("log_format precedence success")
+            else
+                ngx.say("log_format precedence failed: " .. msg)
+            end
+        }
+    }
+--- response_body
+log_format precedence success
+
+
+
+=== TEST 21: route-level log_format_extra overrides the metadata one
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            -- metadata carries one extra field
+            local code, body = t('/apisix/admin/plugin_metadata/file-logger',
+                ngx.HTTP_PUT,
+                [[{
+                    "log_format_extra": {
+                        "meta_only": "from metadata"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            -- the route sets its own, which must fully replace the metadata 
one
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "file-logger": {
+                                "path": "file-logger-override.log",
+                                "log_format_extra": {
+                                    "route_field": "from route"
+                                }
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1982": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 22: only the route extra field is present, metadata one is dropped
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code = t("/hello", ngx.HTTP_GET)
+            local fd, err = io.open("file-logger-override.log", 'r')
+            if not fd then
+                core.log.error("failed to open file: file-logger-override.log, 
error info: ", err)
+                return
+            end
+            local msg = fd:read()
+            fd:close()
+
+            local new_msg = core.json.decode(msg)
+            -- route extra wins, metadata extra is gone, default fields stay
+            if new_msg.route_field == 'from route' and
+               new_msg.meta_only == nil and
+               type(new_msg.request) == "table" and
+               new_msg.request.method == 'GET' and
+               new_msg.route_id == '1'
+            then
+                ngx.status = code
+                ngx.say("route extra precedence success")
+            else
+                ngx.say("route extra precedence failed: " .. msg)
+            end
+        }
+    }
+--- response_body
+route extra precedence success
+
+
+
+=== TEST 23: log_format_extra logs the pre-DNS host for a multi-node domain 
upstream
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/plugin_metadata/file-logger',
+                ngx.HTTP_PUT,
+                [[{
+                    "log_format_extra": {
+                        "upstream_host": "$upstream_unresolved_host"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            -- two domain nodes exercise the server_picker path in balancer.lua
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "plugins": {
+                            "file-logger": {
+                                "path": "file-logger-multinode.log"
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "localhost:1980": 1,
+                                "localhost:1982": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 24: the picked node's pre-DNS host is logged regardless of which one 
is chosen
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local t = require("lib.test_admin").test
+            local code = t("/hello", ngx.HTTP_GET)
+            local fd, err = io.open("file-logger-multinode.log", 'r')
+            if not fd then
+                core.log.error("failed to open file: 
file-logger-multinode.log, error info: ", err)
+                return
+            end
+            local msg = fd:read()
+            fd:close()
+
+            local new_msg = core.json.decode(msg)
+            -- both nodes share the host "localhost", so whichever is picked 
logs it
+            if new_msg.upstream_host == 'localhost' and
+               type(new_msg.request) == "table" and
+               new_msg.request.method == 'GET' and
+               new_msg.route_id == '1'
+            then
+                ngx.status = code
+                ngx.say("enrich log format success")
+            else
+                ngx.say("enrich log format failed: " .. msg)
+            end
+        }
+    }
+--- response_body
+enrich log format success
diff --git a/t/plugin/loki-logger.t b/t/plugin/loki-logger.t
index a72c6f589..89d9b4c8f 100644
--- a/t/plugin/loki-logger.t
+++ b/t/plugin/loki-logger.t
@@ -522,3 +522,97 @@ passed
     }
 --- response_body
 passed
+
+
+
+=== TEST 19: setup route (log_format_extra enriches default via plugin 
metadata)
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            -- additive log format: keep the rich default and add the pre-DNS
+            -- upstream host on top
+            local code, body = t('/apisix/admin/plugin_metadata/loki-logger',
+                ngx.HTTP_PUT,
+                [[{
+                    "log_format_extra": {
+                        "upstream_host": "$upstream_unresolved_host"
+                    }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "loki-logger": {
+                            "endpoint_addrs": ["http://127.0.0.1:3100";],
+                            "tenant_id": "tenant_1",
+                            "log_labels": {
+                                "enrich_label": "enrich_value"
+                            },
+                            "batch_max_size": 1
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 20: hit route
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 21: check loki log (default fields kept + extra field added)
+--- config
+    location /t {
+        content_by_lua_block {
+            local cjson = require("cjson")
+            local now = ngx.now() * 1000
+            local data, err = require("lib.grafana_loki").fetch_logs_from_loki(
+                tostring(now - 3000) .. "000000", -- from
+                tostring(now) .. "000000",        -- to
+                { query = [[{enrich_label="enrich_value"} | json]] }
+            )
+
+            assert(err == nil, "fetch logs error: " .. (err or ""))
+            assert(data.status == "success", "loki response error: " .. 
cjson.encode(data))
+            assert(#data.data.result > 0, "loki log empty: " .. 
cjson.encode(data))
+
+            local entry = data.data.result[1]
+            -- the extra field is added
+            assert(entry.stream.upstream_host == "127.0.0.1",
+                  "expected extra field upstream_host: " .. 
cjson.encode(entry))
+            -- the rich default fields are still present
+            assert(entry.stream.route_id == "1",
+                  "expected default field route_id: " .. cjson.encode(entry))
+            assert(entry.stream.request_method == "GET",
+                  "expected default field request_method: " .. 
cjson.encode(entry))
+        }
+    }
+--- error_code: 200
diff --git a/t/plugin/splunk-hec-logging.t b/t/plugin/splunk-hec-logging.t
index 4610784af..18d606200 100644
--- a/t/plugin/splunk-hec-logging.t
+++ b/t/plugin/splunk-hec-logging.t
@@ -533,3 +533,71 @@ true
 true
 --- no_error_log
 [alert]
+
+
+
+=== TEST 15: log_format_extra surfaces on the default (non-customized) event 
path
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            -- metadata carries only log_format_extra, so the default event 
path runs
+            local code, body = 
t('/apisix/admin/plugin_metadata/splunk-hec-logging',
+                 ngx.HTTP_PUT,
+                 [[{
+                        "log_format_extra": {
+                            "upstream_host": "$upstream_unresolved_host"
+                        }
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {
+                uri = "/hello",
+                upstream = {
+                    type = "roundrobin",
+                    nodes = {
+                        ["127.0.0.1:1982"] = 1
+                    }
+                },
+                plugins = {
+                    ["splunk-hec-logging"] = {
+                        endpoint = {
+                            uri = "http://127.0.0.1:18088/services/collector";,
+                            token = "BD274822-96AA-4DA6-90EC-18940FB2414C"
+                        },
+                        batch_max_size = 1,
+                        inactive_timeout = 1
+                    }
+                }
+            })
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            local code = t("/hello", "GET")
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("fail")
+                return
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+--- wait: 5
+
+
+
+=== TEST 16: check splunk log carries the extra field
+--- exec
+tail -n 1 ci/pod/vector/splunk.log
+--- response_body eval
+qr/.*"upstream_host":"127.0.0.1".*/

Reply via email to