-- -- 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. -- local core = require("apisix.core") local timers = require("apisix.timers") local plugin = require("apisix.plugin") local process = require("ngx.process") local signal = require("resty.signal") local shell = require("resty.shell") local ipairs = ipairs local ngx = ngx local ngx_time = ngx.time local ngx_update_time = ngx.update_time local lfs = require("lfs") local type = type local io_open = io.open local os_date = os.date local os_remove = os.remove local os_rename = os.rename local str_sub = string.sub local str_format = string.format local str_byte = string.byte local ngx_sleep = require("apisix.core.utils").sleep local string_rfind = require("pl.stringx").rfind local local_conf local plugin_name = "log-rotate" local INTERVAL = 60 * 60 -- rotate interval (unit: second) local MAX_KEPT = 24 * 7 -- max number of log files will be kept local MAX_SIZE = -1 -- max size of file will be rotated local COMPRESSION_FILE_SUFFIX = ".tar.gz" -- compression file suffix local rotate_time local default_logs local enable_compression = false local DEFAULT_ACCESS_LOG_FILENAME = "access.log" local DEFAULT_ERROR_LOG_FILENAME = "error.log" local SLASH_BYTE = str_byte("/") local schema = { type = "object", properties = {}, } local _M = { version = 0.1, priority = 100, name = plugin_name, schema = schema, scope = "global", } local function file_exists(path) local file = io_open(path, "r") if file then file:close() end return file ~= nil end local function get_log_path_info(file_type) local_conf = core.config.local_conf() local conf_path if file_type == "error.log" then conf_path = local_conf and local_conf.nginx_config and local_conf.nginx_config.error_log else conf_path = local_conf and local_conf.nginx_config and local_conf.nginx_config.http and local_conf.nginx_config.http.access_log end local prefix = ngx.config.prefix() if conf_path then -- relative path if str_byte(conf_path) ~= SLASH_BYTE then conf_path = prefix .. conf_path end local n = string_rfind(conf_path, "/") if n ~= nil and n ~= #conf_path then local dir = str_sub(conf_path, 1, n) local name = str_sub(conf_path, n + 1) return dir, name end end return prefix .. "logs/", file_type end local function tab_sort_comp(a, b) return a > b end local function scan_log_folder(log_file_name) local t = {} local log_dir, log_name = get_log_path_info(log_file_name) local compression_log_type = log_name .. COMPRESSION_FILE_SUFFIX for file in lfs.dir(log_dir) do local n = string_rfind(file, "__") if n ~= nil then local log_type = file:sub(n + 2) if log_type == log_name or log_type == compression_log_type then core.table.insert(t, file) end end end core.table.sort(t, tab_sort_comp) return t, log_dir end local function rename_file(log, date_str) local new_file if not log.new_file then core.log.warn(log.type, " is off") return end new_file = str_format(log.new_file, date_str) if file_exists(new_file) then core.log.info("file exist: ", new_file) return new_file end local ok, err = os_rename(log.file, new_file) if not ok then core.log.error("move file from ", log.file, " to ", new_file, " res:", ok, " msg:", err) return end return new_file end local function compression_file(new_file, timeout) if not new_file or type(new_file) ~= "string" then core.log.info("compression file: ", new_file, " invalid") return end local n = string_rfind(new_file, "/") local new_filepath = str_sub(new_file, 1, n) local new_filename = str_sub(new_file, n + 1) local com_filename = new_filename .. COMPRESSION_FILE_SUFFIX local cmd = str_format("cd %s && tar -zcf %s %s", new_filepath, com_filename, new_filename) core.log.info("log file compress command: " .. cmd) local ok, stdout, stderr, reason, status = shell.run(cmd, nil, timeout, nil) if not ok then core.log.error("compress log file from ", new_filename, " to ", com_filename, " fail, stdout: ", stdout, " stderr: ", stderr, " reason: ", reason, " status: ", status) return end ok, stderr = os_remove(new_file) if stderr then core.log.error("remove uncompressed log file: ", new_file, " fail, err: ", stderr, " res:", ok) end end local function init_default_logs(logs_info, log_type) local filepath, filename = get_log_path_info(log_type) logs_info[log_type] = { type = log_type } if filename ~= "off" then logs_info[log_type].file = filepath .. filename logs_info[log_type].new_file = filepath .. "/%s__" .. filename end end local function file_size(file) local attr = lfs.attributes(file) if attr then return attr.size end return 0 end local function rotate_file(files, now_time, max_kept, timeout) if core.table.isempty(files) then return end local new_files = core.table.new(2, 0) -- rename the log files for _, file in ipairs(files) do local now_date = os_date("%Y-%m-%d_%H-%M-%S", now_time) local new_file = rename_file(default_logs[file], now_date) if not new_file then return end core.table.insert(new_files, new_file) end -- send signal to reopen log files local pid = process.get_master_pid() core.log.warn("send USR1 signal to master process [", pid, "] for reopening log file") local ok, err = signal.kill(pid, signal.signum("USR1")) if not ok then core.log.error("failed to send USR1 signal for reopening log file: ", err) end if enable_compression then -- Waiting for nginx reopen files -- to avoid losing logs during compression ngx_sleep(0.5) for _, new_file in ipairs(new_files) do compression_file(new_file, timeout) end end for _, file in ipairs(files) do -- clean the oldest file local log_list, log_dir = scan_log_folder(file) for i = max_kept + 1, #log_list do local path = log_dir .. log_list[i] local ok, err = os_remove(path) if err then core.log.error("remove old log file: ", path, " err: ", err, " res:", ok) end end end end local function rotate() local interval = INTERVAL local max_kept = MAX_KEPT local max_size = MAX_SIZE local attr = plugin.plugin_attr(plugin_name) local timeout = 10000 -- default timeout 10 seconds if attr then interval = attr.interval or interval max_kept = attr.max_kept or max_kept max_size = attr.max_size or max_size timeout = attr.timeout or timeout enable_compression = attr.enable_compression or enable_compression end core.log.info("rotate interval:", interval) core.log.info("rotate max keep:", max_kept) core.log.info("rotate max size:", max_size) core.log.info("rotate timeout:", timeout) if not default_logs then -- first init default log filepath and filename default_logs = {} init_default_logs(default_logs, DEFAULT_ACCESS_LOG_FILENAME) init_default_logs(default_logs, DEFAULT_ERROR_LOG_FILENAME) end ngx_update_time() local now_time = ngx_time() if not rotate_time then -- first init rotate time rotate_time = now_time + interval - (now_time % interval) core.log.info("first init rotate time is: ", rotate_time) return end if now_time >= rotate_time then local files = {DEFAULT_ACCESS_LOG_FILENAME, DEFAULT_ERROR_LOG_FILENAME} rotate_file(files, now_time, max_kept, timeout) -- reset rotate time rotate_time = rotate_time + interval elseif max_size > 0 then local access_log_file_size = file_size(default_logs[DEFAULT_ACCESS_LOG_FILENAME].file) local error_log_file_size = file_size(default_logs[DEFAULT_ERROR_LOG_FILENAME].file) local files = core.table.new(2, 0) if access_log_file_size >= max_size then core.table.insert(files, DEFAULT_ACCESS_LOG_FILENAME) end if error_log_file_size >= max_size then core.table.insert(files, DEFAULT_ERROR_LOG_FILENAME) end rotate_file(files, now_time, max_kept, timeout) end end function _M.init() timers.register_timer("plugin#log-rotate", rotate, true) end function _M.destroy() timers.unregister_timer("plugin#log-rotate", true) end return _M