From e6f0af31a70b10709bd20acabbcb634874d0534d Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 9 Jul 2026 18:51:22 +0900 Subject: [PATCH 1/8] feat(file-logger): use io.stdout/io.stderr for /dev/stdout and /dev/stderr paths --- apisix/plugins/file-logger.lua | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/file-logger.lua b/apisix/plugins/file-logger.lua index afe73d662b79..9d7403502f36 100644 --- a/apisix/plugins/file-logger.lua +++ b/apisix/plugins/file-logger.lua @@ -21,6 +21,8 @@ local plugin = require("apisix.plugin") local expr = require("resty.expr.v1") local ngx = ngx local io_open = io.open +local io_stdout = io.stdout +local io_stderr = io.stderr local is_apisix_or, process = pcall(require, "resty.apisix.process") @@ -89,6 +91,12 @@ local _M = { } +local std_files = { + ["/dev/stdout"] = io_stdout, + ["/dev/stderr"] = io_stderr, +} + + local function get_configured_path(conf) if conf.path then return conf.path @@ -152,6 +160,11 @@ if is_apisix_or then end function open_file_cache(conf) + local std_file = std_files[conf.path] + if std_file then + return std_file + end + local last_reopen_time = process.get_last_reopen_ms() local handler, err = path_to_file(conf.path, 0, open_file_handler, conf, {}) @@ -185,7 +198,10 @@ local function write_file_data(conf, log_message) local file, err local file_conf = conf.path and conf or {path = path} - if open_file_cache then + local std_file = std_files[path] + if std_file then + file = std_file + elseif open_file_cache then file, err = open_file_cache(file_conf) else file, err = io_open(path, 'a+') @@ -204,8 +220,9 @@ local function write_file_data(conf, log_message) core.log.error("failed to write file: ", path, ", error info: ", err) end + -- stdout/stderr should not be closed -- file will be closed by gc, if open_file_cache exists - if not open_file_cache then + if not open_file_cache and not std_file then file:close() end end From 090afa30b4e1f253468155aab3c50e495a676d7a Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 9 Jul 2026 20:12:37 +0900 Subject: [PATCH 2/8] feat(file-logger): disable buffering for stdout/stderr handles --- apisix/plugins/file-logger.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apisix/plugins/file-logger.lua b/apisix/plugins/file-logger.lua index 9d7403502f36..d14ecd61966c 100644 --- a/apisix/plugins/file-logger.lua +++ b/apisix/plugins/file-logger.lua @@ -95,6 +95,8 @@ local std_files = { ["/dev/stdout"] = io_stdout, ["/dev/stderr"] = io_stderr, } +io_stdout:setvbuf("no") +io_stderr:setvbuf("no") local function get_configured_path(conf) From 9bb47d1210abc32c16bff3cd5fa6455aaf3de2cd Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 9 Jul 2026 20:12:48 +0900 Subject: [PATCH 3/8] test(file-logger): add cli test for stdout/stderr output --- t/cli/test_file_logger_stdout.sh | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100755 t/cli/test_file_logger_stdout.sh diff --git a/t/cli/test_file_logger_stdout.sh b/t/cli/test_file_logger_stdout.sh new file mode 100755 index 000000000000..6b2271ad9c10 --- /dev/null +++ b/t/cli/test_file_logger_stdout.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +. ./t/cli/common.sh + +# file-logger: write to stdout via /dev/stdout + +echo ' +apisix: + node_listen: 9080 +deployment: + role: data_plane + role_data_plane: + config_provider: yaml +nginx_config: + main_configuration_snippet: | + daemon off; +' > conf/config.yaml + +echo ' +routes: + - uri: /hello + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin + plugins: + file-logger: + path: /dev/stdout +#END +' > conf/apisix.yaml + +make init + +./bin/apisix start > stdout.log 2>&1 & +apisix_pid=$! +sleep 1 + +curl -s http://127.0.0.1:9080/hello > /dev/null +sleep 2 + +kill "$apisix_pid" 2>/dev/null || true +wait "$apisix_pid" 2>/dev/null || true + +if ! grep -q "uri.*hello" stdout.log; then + echo "failed: file-logger did not write to stdout" + cat stdout.log + exit 1 +fi + +echo "passed: file-logger stdout support" From 5d971f712181450f281e864cbdd34f773116d9f4 Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 9 Jul 2026 20:52:43 +0900 Subject: [PATCH 4/8] docs(file-logger): document /dev/stdout and /dev/stderr path support --- docs/en/latest/plugins/file-logger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/latest/plugins/file-logger.md b/docs/en/latest/plugins/file-logger.md index 183ee55b6268..92b6ee0388e3 100644 --- a/docs/en/latest/plugins/file-logger.md +++ b/docs/en/latest/plugins/file-logger.md @@ -45,7 +45,7 @@ The `file-logger` Plugin is used to push log streams to a specific location. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------- | -| path | string | True | Log file path. | +| path | string | True | Log file path. You can use `/dev/stdout` to write logs to the standard output and `/dev/stderr` to write to the standard error output. | | 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 `$`. | | include_req_body | boolean | 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. | From be06a9263ec247fd4f914995bd8a79467d7bf833 Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 16 Jul 2026 21:54:02 +0900 Subject: [PATCH 5/8] refactor(file-logger): remove redundant std_file lookup in open_file_cache std_files is already resolved before open_file_cache is called, so the branch inside open_file_cache was dead code. --- apisix/plugins/file-logger.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apisix/plugins/file-logger.lua b/apisix/plugins/file-logger.lua index d14ecd61966c..f7201f5337d7 100644 --- a/apisix/plugins/file-logger.lua +++ b/apisix/plugins/file-logger.lua @@ -162,11 +162,6 @@ if is_apisix_or then end function open_file_cache(conf) - local std_file = std_files[conf.path] - if std_file then - return std_file - end - local last_reopen_time = process.get_last_reopen_ms() local handler, err = path_to_file(conf.path, 0, open_file_handler, conf, {}) From c959f3a7921a690e8b3fc078448e4f61c956c6ae Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 16 Jul 2026 21:54:02 +0900 Subject: [PATCH 6/8] test(file-logger): capture process stdout by running openresty directly `apisix start` launches nginx via io.popen and consumes its stdout, so it cannot observe /dev/stdout output. Run openresty in the foreground like the container entrypoint does, and wait for readiness instead of a fixed sleep. --- t/cli/test_file_logger_stdout.sh | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/t/cli/test_file_logger_stdout.sh b/t/cli/test_file_logger_stdout.sh index 6b2271ad9c10..42946c26e47b 100755 --- a/t/cli/test_file_logger_stdout.sh +++ b/t/cli/test_file_logger_stdout.sh @@ -19,7 +19,13 @@ . ./t/cli/common.sh -# file-logger: write to stdout via /dev/stdout +# file-logger: verify logs are written to the standard output via "/dev/stdout". +# +# APISIX in a container runs openresty directly in the foreground +# (see docker/debian-dev/docker-entrypoint.sh: `openresty -p ... -g 'daemon off;'`), +# so its stdout is the container's stdout. We replicate that here to capture +# the process stdout. (`apisix start` launches nginx via io.popen and consumes +# its stdout, so it cannot be used to observe /dev/stdout output.) echo ' apisix: @@ -28,9 +34,6 @@ deployment: role: data_plane role_data_plane: config_provider: yaml -nginx_config: - main_configuration_snippet: | - daemon off; ' > conf/config.yaml echo ' @@ -47,21 +50,32 @@ routes: ' > conf/apisix.yaml make init +# `apisix start` would create the logs directory; since we launch openresty +# directly, create it ourselves so nginx can open its error log. +mkdir -p logs -./bin/apisix start > stdout.log 2>&1 & +# run openresty directly in the foreground and capture its stdout +openresty -p "$PWD" -g 'daemon off;' > stdout.log 2> stderr.log & apisix_pid=$! -sleep 1 + +# wait until APISIX is ready to serve +for _ in $(seq 1 20); do + if curl -s -o /dev/null http://127.0.0.1:9080/hello; then + break + fi + sleep 0.5 +done curl -s http://127.0.0.1:9080/hello > /dev/null -sleep 2 +sleep 1 kill "$apisix_pid" 2>/dev/null || true wait "$apisix_pid" 2>/dev/null || true -if ! grep -q "uri.*hello" stdout.log; then +if ! grep -q '"uri":"/hello"' stdout.log; then echo "failed: file-logger did not write to stdout" cat stdout.log exit 1 fi -echo "passed: file-logger stdout support" +echo "passed: file-logger writes to stdout" From d62672fc0010af4264aeed42811fb4077fb74d0b Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 16 Jul 2026 21:54:02 +0900 Subject: [PATCH 7/8] docs(file-logger): document /dev/stdout and /dev/stderr path support in zh --- docs/zh/latest/plugins/file-logger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/latest/plugins/file-logger.md b/docs/zh/latest/plugins/file-logger.md index 3509b697ffcc..a128e574bfe2 100644 --- a/docs/zh/latest/plugins/file-logger.md +++ b/docs/zh/latest/plugins/file-logger.md @@ -47,7 +47,7 @@ description: API 网关 Apache APISIX file-logger 插件可用于将日志数据 | 名称 | 类型 | 必选项 | 描述 | | ---------------- | ------- |-----| ------------------------------------------------ | -| path | string | 是 | 自定义输出文件路径。例如:`logs/file.log`。 | +| path | string | 是 | 自定义输出文件路径。例如:`logs/file.log`。可以使用 `/dev/stdout` 将日志写入标准输出,使用 `/dev/stderr` 将日志写入标准错误输出。 | | log_format | object | 否 | 日志格式以 JSON 的键值对声明。值支持字符串和嵌套对象(最多五层,超出部分将被截断)。字符串中可通过在前面加上 `$` 来引用 [APISIX 变量](../apisix-variable.md) 或 [NGINX 内置变量](http://nginx.org/en/docs/varindex.html)。 | | include_req_body | boolean | 否 | 当设置为 `true` 时,日志中将包含请求体。如果请求体太大而无法在内存中保存,则由于 Nginx 的限制,无法记录请求体。| | include_req_body_expr | array | 否 | 当 `include_req_body` 属性设置为 `true` 时的过滤器。只有当此处设置的表达式求值为 `true` 时,才会记录请求体。有关更多信息,请参阅 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 。 | From 6f9c542618f3841603ba0024120acd4c0fe2ff2c Mon Sep 17 00:00:00 2001 From: ecsimsw Date: Thu, 16 Jul 2026 22:52:22 +0900 Subject: [PATCH 8/8] style(file-logger): shorten test comment to match project convention --- t/cli/test_file_logger_stdout.sh | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/t/cli/test_file_logger_stdout.sh b/t/cli/test_file_logger_stdout.sh index 42946c26e47b..681c1237ed48 100755 --- a/t/cli/test_file_logger_stdout.sh +++ b/t/cli/test_file_logger_stdout.sh @@ -19,13 +19,9 @@ . ./t/cli/common.sh -# file-logger: verify logs are written to the standard output via "/dev/stdout". -# -# APISIX in a container runs openresty directly in the foreground -# (see docker/debian-dev/docker-entrypoint.sh: `openresty -p ... -g 'daemon off;'`), -# so its stdout is the container's stdout. We replicate that here to capture -# the process stdout. (`apisix start` launches nginx via io.popen and consumes -# its stdout, so it cannot be used to observe /dev/stdout output.) +# file-logger: verify logs are written to stdout via "/dev/stdout". +# Uses openresty directly (like docker-entrypoint.sh) because `apisix start` +# consumes stdout via io.popen, making /dev/stdout output unobservable. echo ' apisix: