diff --git a/doc/admin-guide/files/logging.yaml.en.rst b/doc/admin-guide/files/logging.yaml.en.rst index d7e076add49..0f9cee03550 100644 --- a/doc/admin-guide/files/logging.yaml.en.rst +++ b/doc/admin-guide/files/logging.yaml.en.rst @@ -266,7 +266,10 @@ Name Type Description ====================== =========== ================================================= filename string The name of the logfile relative to the default logging directory (set with - :ts:cv:`proxy.config.log.logfile_dir`). + :ts:cv:`proxy.config.log.logfile_dir`). If + this is set to ``stdout`` or ``stderr``, + then the stdout or stderr stream will be + logged to, respectively. format string a string with a valid named format specification. header string If present, emitted as the first line of each new log file. diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst index 1972742f940..e9c9bd49ed0 100644 --- a/doc/admin-guide/files/records.config.en.rst +++ b/doc/admin-guide/files/records.config.en.rst @@ -531,6 +531,14 @@ Network Local Manager ============= +.. ts:cv:: CONFIG proxy.node.config.manager_log_filename STRING manager.log + + The name of the file to which :program:`traffic_manager` logs will be emitted. + + If this is set to ``stdout`` or ``stderr``, then all :program:`traffic_manager` + logging will go to the stdout or stderr stream, respectively. + + .. ts:cv:: CONFIG proxy.config.admin.user_id STRING nobody Designates the non-privileged account to run the :program:`traffic_server` @@ -3152,6 +3160,8 @@ Logging Configuration Diagnostic Logging Configuration ================================ +.. _DiagnosticOutputConfigurationVariables: + .. ts:cv:: CONFIG proxy.config.diags.output.diag STRING E .. ts:cv:: CONFIG proxy.config.diags.output.debug STRING E .. ts:cv:: CONFIG proxy.config.diags.output.status STRING L @@ -3173,7 +3183,8 @@ Diagnostic Logging Configuration ``O`` Log to standard output. ``E`` Log to standard error. ``S`` Log to syslog. - ``L`` Log to :file:`diags.log`. + ``L`` Log to :file:`diags.log` (with the filename configurable via + :ts:cv:`proxy.config.diags.logfile.filename`). ===== ====================================================================== .. topic:: Example @@ -3231,6 +3242,25 @@ Diagnostic Logging Configuration :ts:cv:`log.throttling_interval_msec `. +.. ts:cv:: CONFIG proxy.config.diags.logfile.filename STRING diags.log + + The name of the file to which |TS| diagnostic logs will be emitted. For + information on the diagnostic log file, see :file:`diags.log`. For the + configurable parameters concerning what log content is emitted to + :file:`diags.log`, see the :ref:`Diagnostic Output Configuration Variables + ` above. + + If this is set to ``stdout`` or ``stderr``, then all diagnostic logging will + go to the stdout or stderr stream, respectively. + +.. ts:cv:: CONFIG proxy.config.error.logfile.filename STRING error.log + + The name of the file to which |TS| transaction error logs will be emitted. + For more information on these log messages, see :file:`error.log`. + + If this is set to ``stdout`` or ``stderr``, then all transaction error + logging will go to the stdout or stderr stream, respectively. + .. ts:cv:: CONFIG proxy.config.diags.logfile_perm STRING rw-r--r-- The log file permissions. The standard UNIX file permissions are used (owner, group, other). Permissible values are: diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc index b7c5ca62829..d5e089c3a5f 100644 --- a/mgmt/RecordsConfig.cc +++ b/mgmt/RecordsConfig.cc @@ -228,6 +228,8 @@ static const RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.diags.logfile_perm", RECD_STRING, "rw-r--r--", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , + {RECT_CONFIG, "proxy.config.diags.logfile.filename", RECD_STRING, "diags.log", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[^[:space:]]+$", RECA_NULL} + , // diags.log rotation, default is 0 (aka rolling turned off) to preserve compatibility {RECT_CONFIG, "proxy.config.diags.logfile.rolling_enabled", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-2]", RECA_NULL} , @@ -237,6 +239,8 @@ static const RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.diags.logfile.rolling_min_count", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_STR, "^0*[1-9][0-9]*$", RECA_NULL} , + {RECT_CONFIG, "proxy.config.error.logfile.filename", RECD_STRING, "error.log", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[^[:space:]]+$", RECA_NULL} + , //############################################################################## //# @@ -1276,6 +1280,9 @@ static const RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.node.config.manager_retry_cap", RECD_INT, "5", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , + //# manager: log filename + {RECT_CONFIG, "proxy.node.config.manager_log_filename", RECD_STRING, "manager.log", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[^[:space:]]+$", RECA_NULL} + , //# //# SSL parent proxying info diff --git a/proxy/logging/LogConfig.cc b/proxy/logging/LogConfig.cc index 7676a6e3ab7..449180d344a 100644 --- a/proxy/logging/LogConfig.cc +++ b/proxy/logging/LogConfig.cc @@ -79,6 +79,7 @@ LogConfig::setup_default_values() max_secs_per_buffer = 5; max_space_mb_for_logs = 100; max_space_mb_headroom = 10; + error_log_filename = ats_strdup("error.log"); logfile_perm = 0644; logfile_dir = ats_strdup("."); @@ -165,6 +166,12 @@ LogConfig::read_configuration_variables() hostname = ptr; } + ptr = REC_ConfigReadString("proxy.config.error.logfile.filename"); + if (ptr != nullptr) { + ats_free(error_log_filename); + error_log_filename = ptr; + } + ats_free(logfile_dir); logfile_dir = ats_stringdup(RecConfigReadLogDir()); @@ -292,6 +299,7 @@ LogConfig::LogConfig() : m_partition_space_left(static_cast(UINT_MAX)) LogConfig::~LogConfig() { + ats_free(error_log_filename); ats_free(logfile_dir); } @@ -321,9 +329,9 @@ LogConfig::init(LogConfig *prev_config) Debug("log", "creating predefined error log object"); - errlog = new LogObject(this, fmt.get(), logfile_dir, "error.log", LOG_FILE_ASCII, nullptr, rolling_enabled, preproc_threads, - rolling_interval_sec, rolling_offset_hr, rolling_size_mb, /* auto_created */ false, rolling_max_count, - rolling_min_count); + errlog = new LogObject(this, fmt.get(), logfile_dir, error_log_filename, LOG_FILE_ASCII, nullptr, rolling_enabled, + preproc_threads, rolling_interval_sec, rolling_offset_hr, rolling_size_mb, /* auto_created */ false, + rolling_max_count, rolling_min_count); log_object_manager.manage_object(errlog); errlog->set_fmt_timestamps(); @@ -367,6 +375,7 @@ LogConfig::display(FILE *fd) fprintf(fd, " hostname = %s\n", hostname); fprintf(fd, " logfile_dir = %s\n", logfile_dir); fprintf(fd, " logfile_perm = 0%o\n", logfile_perm); + fprintf(fd, " error_log_filename = %s\n", error_log_filename); fprintf(fd, " preproc_threads = %d\n", preproc_threads); fprintf(fd, " rolling_enabled = %d\n", rolling_enabled); @@ -454,6 +463,7 @@ LogConfig::register_config_callbacks() "proxy.config.log.max_secs_per_buffer", "proxy.config.log.max_space_mb_for_logs", "proxy.config.log.max_space_mb_headroom", + "proxy.config.log.error_log_filename", "proxy.config.log.logfile_perm", "proxy.config.log.hostname", "proxy.config.log.logfile_dir", diff --git a/proxy/logging/LogConfig.h b/proxy/logging/LogConfig.h index 858d64e8cd9..9a8e7cd504e 100644 --- a/proxy/logging/LogConfig.h +++ b/proxy/logging/LogConfig.h @@ -207,8 +207,9 @@ class LogConfig : public ConfigInfo int max_line_size; int logbuffer_max_iobuf_index; - char *hostname; - char *logfile_dir; + char *hostname = nullptr; + char *logfile_dir = nullptr; + char *error_log_filename = nullptr; private: bool evaluate_config(); diff --git a/proxy/logging/LogObject.cc b/proxy/logging/LogObject.cc index 139dab75c59..260a7c8a285 100644 --- a/proxy/logging/LogObject.cc +++ b/proxy/logging/LogObject.cc @@ -155,11 +155,13 @@ LogObject::~LogObject() // // This function generates an object filename according to the following rules: // -// 1.- if no extension is given, add .log for ascii logs, and .blog for +// 1.- 'stdout' and 'stderr' are treated as special strings indicating file +// descriptors for the stdout and stderr streams. +// 2.- if no extension is given, add .log for ascii logs, and .blog for // binary logs -// 2.- if an extension is given, then do not modify filename and use that +// 3.- if an extension is given, then do not modify filename and use that // extension regardless of type of log -// 3.- if there is a '.' at the end of the name, then do not add an extension +// 4.- if there is a '.' at the end of the name, then do not add an extension // and remove the '.'. To have a dot at the end of the filename, specify // two ('..'). // @@ -168,6 +170,18 @@ LogObject::generate_filenames(const char *log_dir, const char *basename, LogFile { ink_assert(log_dir && basename); + std::string_view basename_sv{basename}; + if (basename_sv == "stdout" || basename_sv == "stderr") { + int buffer_size = basename_sv.length() + 1; // Include the null terminator. + + m_filename = static_cast(ats_malloc(buffer_size)); + m_basename = static_cast(ats_malloc(buffer_size)); + + strncpy(m_filename, basename, buffer_size); + strncpy(m_basename, basename, buffer_size); + return; + } + int i = -1, len = 0; char c; while (c = basename[len], c != 0) { @@ -261,6 +275,10 @@ LogObject::set_filter_list(const LogFilterList &list, bool copy) uint64_t LogObject::compute_signature(LogFormat *format, char *filename, unsigned int flags) { + std::string_view filename_sv{filename}; + if (filename_sv == "stdout" || filename_sv == "stderr") { + return 0; + } char *fl = format->fieldlist(); char *ps = format->printf_str(); uint64_t signature = 0; @@ -983,14 +1001,17 @@ LogObjectManager::_filename_resolution_abort(const char *filename) } bool -LogObjectManager::_has_internal_filename_conflict(const char *filename, LogObjectList &objects) +LogObjectManager::_has_internal_filename_conflict(std::string_view filename, LogObjectList &objects) { + if (filename == "stdout" || filename == "stderr") { + return false; + } for (auto &object : objects) { // an internal conflict exists if two objects request the // same filename, regardless of the object signatures, since // two objects writing to the same file would produce a // log with duplicate entries and non monotonic timestamps - if (strcmp(object->get_full_filename(), filename) == 0) { + if (filename == object->get_full_filename()) { return true; } } diff --git a/proxy/logging/LogObject.h b/proxy/logging/LogObject.h index 9b57340071f..c2395371f9f 100644 --- a/proxy/logging/LogObject.h +++ b/proxy/logging/LogObject.h @@ -352,7 +352,7 @@ class LogObjectManager ink_mutex *_APImutex; // synchronize access to array of API objects private: int _manage_object(LogObject *log_object, bool is_api_object, int maxConflicts); - static bool _has_internal_filename_conflict(const char *filename, LogObjectList &objects); + static bool _has_internal_filename_conflict(std::string_view filename, LogObjectList &objects); int _solve_filename_conflicts(LogObject *log_obj, int maxConflicts); int _solve_internal_filename_conflicts(LogObject *log_obj, int maxConflicts, int fileNum = 0); void _filename_resolution_abort(const char *fname); diff --git a/proxy/shared/DiagsConfig.cc b/proxy/shared/DiagsConfig.cc index 305330c7a40..7b50ebde269 100644 --- a/proxy/shared/DiagsConfig.cc +++ b/proxy/shared/DiagsConfig.cc @@ -249,7 +249,6 @@ DiagsConfig::DiagsConfig(std::string_view prefix_string, const char *filename, c bool use_records) : callbacks_established(false), diags_log(nullptr), _diags(nullptr) { - char diags_logpath[PATH_NAME_MAX]; ats_scoped_str logpath; //////////////////////////////////////////////////////////////////// @@ -275,7 +274,16 @@ DiagsConfig::DiagsConfig(std::string_view prefix_string, const char *filename, c ::exit(1); } - ink_filepath_make(diags_logpath, sizeof(diags_logpath), logpath, filename); + std::string diags_logpath{filename}; + // "stdout" and "stderr" are treated specially by BaseLogFile and are used to + // write to the stdout and stderr streams, respectively. If the caller + // specified these, we don't prepend any path and simply pass those strings + // as such to BaseLogFile. + if (diags_logpath != "stdout" && diags_logpath != "stderr") { + char buf[PATH_NAME_MAX]; + ink_filepath_make(buf, sizeof(buf), logpath, filename); + diags_logpath = std::string(buf); + } // Grab rolling intervals from configuration // TODO error check these values @@ -296,14 +304,14 @@ DiagsConfig::DiagsConfig(std::string_view prefix_string, const char *filename, c ats_free(output_perm); // Set up diags, FILE streams are opened in Diags constructor - diags_log = new BaseLogFile(diags_logpath); + diags_log = new BaseLogFile(diags_logpath.c_str()); _diags = new Diags(prefix_string, tags, actions, diags_log, diags_perm_parsed, output_perm_parsed); ::diags = _diags; _diags->config_roll_diagslog(static_cast(diags_log_roll_enable), diags_log_roll_int, diags_log_roll_size); _diags->config_roll_outputlog(static_cast(output_log_roll_enable), output_log_roll_int, output_log_roll_size); - Status("opened %s", diags_logpath); + Status("opened %s", diags_logpath.c_str()); register_diags_callbacks(); diff --git a/src/traffic_manager/traffic_manager.cc b/src/traffic_manager/traffic_manager.cc index aa1d40a50c3..48392935895 100644 --- a/src/traffic_manager/traffic_manager.cc +++ b/src/traffic_manager/traffic_manager.cc @@ -21,6 +21,7 @@ limitations under the License. */ +#include "tscore/ink_platform.h" #include "tscore/ink_sys_control.h" #include "tscore/ink_cap.h" #include "tscore/ink_lockfile.h" @@ -62,7 +63,8 @@ #include "tscore/bwf_std_format.h" #define FD_THROTTLE_HEADROOM (128 + 64) // TODO: consolidate with THROTTLE_FD_HEADROOM -#define DIAGS_LOG_FILENAME "manager.log" +#define DEFAULT_DIAGS_LOG_FILENAME "manager.log" +static char diags_log_filename[PATH_NAME_MAX] = DEFAULT_DIAGS_LOG_FILENAME; #if ATOMIC_INT_LOCK_FREE != 2 #error "Need lock free std::atomic" @@ -131,7 +133,7 @@ rotateLogs() // Now we can actually roll the logs (if necessary) if (diags->should_roll_diagslog()) { - mgmt_log("Rotated %s", DIAGS_LOG_FILENAME); + mgmt_log("Rotated %s", diags_log_filename); } if (diags->should_roll_outputlog()) { @@ -545,7 +547,7 @@ main(int argc, const char **argv) // Bootstrap the Diags facility so that we can use it while starting // up the manager - diagsConfig = new DiagsConfig("Manager", DIAGS_LOG_FILENAME, debug_tags, action_tags, false); + diagsConfig = new DiagsConfig("Manager", DEFAULT_DIAGS_LOG_FILENAME, debug_tags, action_tags, false); diags->set_std_output(StdStream::STDOUT, bind_stdout); diags->set_std_output(StdStream::STDERR, bind_stderr); @@ -591,8 +593,12 @@ main(int argc, const char **argv) // INKqa11968: need to set up callbacks and diags data structures // using configuration in records.config + REC_ReadConfigString(diags_log_filename, "proxy.node.config.manager_log_filename", sizeof(diags_log_filename)); + if (strnlen(diags_log_filename, sizeof(diags_log_filename)) == 0) { + strncpy(diags_log_filename, DEFAULT_DIAGS_LOG_FILENAME, sizeof(diags_log_filename)); + } DiagsConfig *old_diagsconfig = diagsConfig; - diagsConfig = new DiagsConfig("Manager", DIAGS_LOG_FILENAME, debug_tags, action_tags, true); + diagsConfig = new DiagsConfig("Manager", diags_log_filename, debug_tags, action_tags, true); if (old_diagsconfig) { delete old_diagsconfig; old_diagsconfig = nullptr; @@ -731,7 +737,7 @@ main(int argc, const char **argv) lmgmt->processEventQueue(); lmgmt->pollMgmtProcessServer(); - // Handle rotation of output log (aka traffic.out) as well as DIAGS_LOG_FILENAME (aka manager.log) + // Handle rotation of output log (aka traffic.out) as well as DEFAULT_DIAGS_LOG_FILENAME (aka manager.log) rotateLogs(); // Check for a SIGHUP @@ -937,9 +943,9 @@ SignalHandler(int sig) diags->set_std_output(StdStream::STDOUT, bind_stdout); diags->set_std_output(StdStream::STDERR, bind_stderr); if (diags->reseat_diagslog()) { - Note("Reseated %s", DIAGS_LOG_FILENAME); + Note("Reseated %s", diags_log_filename); } else { - Note("Could not reseat %s", DIAGS_LOG_FILENAME); + Note("Could not reseat %s", diags_log_filename); } return; } diff --git a/src/traffic_server/traffic_server.cc b/src/traffic_server/traffic_server.cc index 887eecbb317..e56075a61cf 100644 --- a/src/traffic_server/traffic_server.cc +++ b/src/traffic_server/traffic_server.cc @@ -122,7 +122,8 @@ extern "C" int plock(int); #define DEFAULT_COMMAND_FLAG 0 #define DEFAULT_REMOTE_MANAGEMENT_FLAG 0 -#define DIAGS_LOG_FILENAME "diags.log" +#define DEFAULT_DIAGS_LOG_FILENAME "diags.log" +static char diags_log_filename[PATH_NAME_MAX] = DEFAULT_DIAGS_LOG_FILENAME; static const long MAX_LOGIN = ink_login_name_max(); @@ -296,9 +297,9 @@ class SignalContinuation : public Continuation diags->set_std_output(StdStream::STDOUT, bind_stdout); diags->set_std_output(StdStream::STDERR, bind_stderr); if (diags->reseat_diagslog()) { - Note("Reseated %s", DIAGS_LOG_FILENAME); + Note("Reseated %s", diags_log_filename); } else { - Note("Could not reseat %s", DIAGS_LOG_FILENAME); + Note("Could not reseat %s", diags_log_filename); } // Reload any of the other moved log files (such as the ones in logging.yaml). Log::handle_log_rotation_request(); @@ -407,7 +408,7 @@ class DiagsLogContinuation : public Continuation diags->config_roll_diagslog((RollingEnabledValues)diags_log_roll_enable, diags_log_roll_int, diags_log_roll_size); if (diags->should_roll_diagslog()) { - Note("Rolled %s", DIAGS_LOG_FILENAME); + Note("Rolled %s", diags_log_filename); } return EVENT_CONT; } @@ -1794,7 +1795,7 @@ main(int /* argc ATS_UNUSED */, const char **argv) // re-start it again, TS will crash. // This is also needed for log rotation - setting up the file can cause privilege // related errors and if diagsConfig isn't get up yet that will crash on a NULL pointer. - diagsConfig = new DiagsConfig("Server", DIAGS_LOG_FILENAME, error_tags, action_tags, false); + diagsConfig = new DiagsConfig("Server", DEFAULT_DIAGS_LOG_FILENAME, error_tags, action_tags, false); diags->set_std_output(StdStream::STDOUT, bind_stdout); diags->set_std_output(StdStream::STDERR, bind_stderr); if (is_debug_tag_set("diags")) { @@ -1887,8 +1888,12 @@ main(int /* argc ATS_UNUSED */, const char **argv) main_thread->set_specific(); // Re-initialize diagsConfig based on records.config configuration + REC_ReadConfigString(diags_log_filename, "proxy.config.diags.logfile.filename", sizeof(diags_log_filename)); + if (strnlen(diags_log_filename, sizeof(diags_log_filename)) == 0) { + strncpy(diags_log_filename, DEFAULT_DIAGS_LOG_FILENAME, sizeof(diags_log_filename)); + } DiagsConfig *old_log = diagsConfig; - diagsConfig = new DiagsConfig("Server", DIAGS_LOG_FILENAME, error_tags, action_tags, true); + diagsConfig = new DiagsConfig("Server", diags_log_filename, error_tags, action_tags, true); RecSetDiags(diags); diags->set_std_output(StdStream::STDOUT, bind_stdout); diags->set_std_output(StdStream::STDERR, bind_stderr); diff --git a/tests/gold_tests/autest-site/trafficserver.test.ext b/tests/gold_tests/autest-site/trafficserver.test.ext index 502d65ef228..573e3255212 100755 --- a/tests/gold_tests/autest-site/trafficserver.test.ext +++ b/tests/gold_tests/autest-site/trafficserver.test.ext @@ -25,13 +25,22 @@ def make_id(s): return s.replace(".", "_").replace('-', '_') -# this forms is for the global process define +# A mapping from log type to the log name. 'stdout' and 'stderr' are handled +# specially and are used to indicate the stdout and stderr streams, +# respectively. +default_log_data = { + 'diags': 'diags.log', + 'error': 'error.log', + 'manager': 'manager.log' +} # 'block_for_debug', if True, causes traffic_server to run with the --block option enabled, and effectively # disables timeouts that could be triggered by running traffic_server under a debugger. + + def MakeATSProcess(obj, name, command='traffic_server', select_ports=True, enable_tls=False, enable_cache=True, enable_quic=False, - block_for_debug=False): + block_for_debug=False, log_data=default_log_data): ##################################### # common locations @@ -185,26 +194,41 @@ def MakeATSProcess(obj, name, command='traffic_server', select_ports=True, tmpname = os.path.join(log_dir, fname) p.Disk.File(tmpname, id=make_id(fname)) # error.log - fname = "error.log" - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), exists=False) + fname = log_data['error'] + if fname == 'stdout': + p.Disk.error_log = p.Streams.stdout + elif fname == 'stderr': + p.Disk.error_log = p.Streams.stderr + else: + tmpname = os.path.join(log_dir, fname) + p.Disk.File(tmpname, id='error_log', exists=False) # diags.log - fname = "diags.log" - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id=make_id(fname)) + fname = log_data['diags'] + if fname == 'stdout': + p.Disk.diags_log = p.Streams.stdout + elif fname == 'stderr': + p.Disk.diags_log = p.Streams.stderr + else: + tmpname = os.path.join(log_dir, fname) + p.Disk.File(tmpname, id='diags_log') # add this test back once we have network namespaces working again p.Disk.diags_log.Content = Testers.ExcludesExpression( - "ERROR:", "diags.log should not contain errors") + "ERROR:", f"Diags log file {fname} should not contain errors") p.Disk.diags_log.Content += Testers.ExcludesExpression( - "FATAL:", "diags.log should not contain errors") + "FATAL:", f"Diags log file {fname} should not contain errors") p.Disk.diags_log.Content += Testers.ExcludesExpression( "Unrecognized configuration value", - "diags.log should not contain a warning about an unrecognized configuration") + f"Diags log file {fname} should not contain a warning about an unrecognized configuration") if command == "traffic_manager": - fname = "manager.log" - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id=make_id(fname)) + fname = log_data['manager'] + if fname == 'stdout': + p.Disk.manager_log = p.Streams.stdout + elif fname == 'stderr': + p.Disk.manager_log = p.Streams.stderr + else: + tmpname = os.path.join(log_dir, fname) + p.Disk.File(tmpname, id='manager_log') # config files def MakeConfigFile(self, fname): diff --git a/tests/gold_tests/logging/log-filenames.test.py b/tests/gold_tests/logging/log-filenames.test.py new file mode 100644 index 00000000000..df3888e9896 --- /dev/null +++ b/tests/gold_tests/logging/log-filenames.test.py @@ -0,0 +1,303 @@ +''' +Verify log file naming behavior. +''' +# 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. + +import os +import ports + +Test.Summary = ''' +Verify log file naming behavior. +''' + + +class LogFilenamesTest: + """ Common test configuration logic across the filename tests. + """ + + # A counter for the ATS process to make each of them unique. + __ts_counter = 1 + + # The default log names for the various system logs. + default_log_data = { + 'diags': 'diags.log', + 'error': 'error.log', + 'manager': 'manager.log' + } + + def __init__(self, description, log_data=default_log_data): + ''' Handle initialization tasks common across the tests. + + Args: + description (str): The description of the test. This is passed to + the TestRun. + + log_data (dict): The log name information passed to the + MakeATSProcess extension. + ''' + self.__description = description + self.ts = self.__configure_traffic_manager(log_data) + self.tr = self.__configure_traffic_TestRun(description) + self.__configure_await_TestRun(self.sentinel_log_path) + + def __configure_traffic_manager(self, log_data): + ''' Common ATS configuration logic. + + Args: + log_data (dict): The log name information passed to the + MakeATSProcess extension. + + Return: + The traffic_manager process. + ''' + self._ts_name = f"ts{LogFilenamesTest.__ts_counter}" + LogFilenamesTest.__ts_counter += 1 + self.ts = Test.MakeATSProcess(self._ts_name, command="traffic_manager", + log_data=log_data) + self.ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'log', + 'proxy.config.log.periodic_tasks_interval': 1, + }) + + # Intentionally retrieve a port that is closed, that is no server is + # listening on it. We will use this to attempt talking with a + # non-existent server, which will result in an error log entry. + ports.get_port(self.ts, 'closed_port') + self.ts.Disk.remap_config.AddLines([ + f'map /server/down http://127.0.0.1:{self.ts.Variables.closed_port}', + 'map / https://trafficserver.apache.org @action=deny', + ]) + + # The following log is configured so that we can wait upon it being + # written so we know that ATS is done writing logs. + self.sentinel_log_filename = "sentinel" + self.ts.Disk.logging_yaml.AddLine(f''' + logging: + formats: + - name: url_and_return_code + format: "%: %" + logs: + - filename: {self.sentinel_log_filename} + format: url_and_return_code + ''') + + self.sentinel_log_path = os.path.join( + self.ts.Variables.LOGDIR, + f"{self.sentinel_log_filename}.log") + + return self.ts + + def __configure_await_TestRun(self, log_path): + ''' Configure a TestRun that awaits upon the provided log_path to + exist. + + Args: + log_path (str): The log file upon which we will wait. + ''' + description = self.__description + tr = Test.AddTestRun(f'Awaiting log files to be written for: {description}') + condwait_path = os.path.join(Test.Variables.AtsTestToolsDir, 'condwait') + tr.Processes.Default.Command = f'{condwait_path} 60 1 -f {log_path}' + tr.Processes.Default.ReturnCode = 0 + + def __configure_traffic_TestRun(self, description): + ''' Configure a TestRun to run the expected transactions. + + Args: + description (str): The description to use for the TestRun. + ''' + tr = Test.AddTestRun(f'Run traffic for: {description}') + tr.Processes.Default.Command = ( + f'curl http://127.0.0.1:{self.ts.Variables.port}/some/path --verbose --next ' + f'curl http://127.0.0.1:{self.ts.Variables.port}/server/down --verbose' + ) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.ts) + + def configure_named_custom_log(self, custom_log_filename): + """ Configure ATS to log to the custom log file via logging.yaml. + + Args: + custom_log_filename (str): The name of the custom log file to + configure. + + Return: + The path to the configured custom log file. + """ + self.custom_log_filename = custom_log_filename + self.ts.Disk.logging_yaml.AddLine(f''' + - filename: {custom_log_filename} + format: url_and_return_code + ''') + + if custom_log_filename in ('stdout', 'stderr'): + self.custom_log_path = custom_log_filename + if custom_log_filename == 'stdout': + self.ts.Disk.custom_log = self.ts.Streams.stdout + else: + self.ts.Disk.custom_log = self.ts.Streams.stderr + else: + self.custom_log_path = os.path.join( + self.ts.Variables.LOGDIR, + f"{custom_log_filename}.log") + self.ts.Disk.File(self.custom_log_path, id="custom_log") + return self.custom_log_path + + def set_log_expectations(self): + ''' Configure sanity checks for each of the log types (manager, error, + etc.) to verify they are emitting the expected content. + ''' + manager_path = self.ts.Disk.manager_log.AbsPath + self.ts.Disk.manager_log.Content += Testers.ContainsExpression( + "Launching ts process", + f"{manager_path} should contain traffic_manager log messages") + + diags_path = self.ts.Disk.diags_log.AbsPath + self.ts.Disk.diags_log.Content += Testers.ContainsExpression( + "Traffic Server is fully initialized", + f"{diags_path} should contain traffic_server diag messages") + + error_log_path = self.ts.Disk.error_log.AbsPath + self.ts.Disk.error_log.Content += Testers.ContainsExpression( + "CONNECT: attempt fail", + f"{error_log_path} should contain connection error messages") + + custom_log_path = self.ts.Disk.custom_log.AbsPath + self.ts.Disk.custom_log.Content += Testers.ContainsExpression( + "https://trafficserver.apache.org/some/path: 403", + f"{custom_log_path} should contain the custom transaction logs") + + +class DefaultNamedTest(LogFilenamesTest): + ''' Verify that if custom names are not configured, then the default + 'diags.log', 'manager.log', and 'error.log' are written to. + ''' + + def __init__(self): + super().__init__('default log filename configuration') + + # For these tests, more important than the listening port is the + # existence of the log files. In particular, it can take a few seconds + # for traffic_manager to open diags.log. + self.diags_log = self.ts.Disk.diags_log.AbsPath + self.ts.Ready = When.FileExists(self.diags_log) + + self.configure_named_custom_log('my_custom_log') + self.set_log_expectations() + + +class CustomNamedTest(LogFilenamesTest): + ''' Verify that the user can assign custom filenames to manager.log, etc. + ''' + + def __init__(self): + log_data = { + 'diags': 'my_diags.log', + 'error': 'my_error.log', + 'manager': 'my_manager.log' + } + super().__init__('specify log filename configuration', log_data) + + # Configure custom names for manager.log, etc. + self.ts.Disk.records_config.update({ + 'proxy.node.config.manager_log_filename': 'my_manager.log', + 'proxy.config.diags.logfile.filename': 'my_diags.log', + 'proxy.config.error.logfile.filename': 'my_error.log', + }) + + # For these tests, more important than the listening port is the + # existence of the log files. In particular, it can take a few seconds + # for traffic_manager to open diags.log. + self.diags_log = self.ts.Disk.diags_log.AbsPath + self.ts.Ready = When.FileExists(self.diags_log) + + self.configure_named_custom_log('my_custom_log') + self.set_log_expectations() + + +class stdoutTest(LogFilenamesTest): + ''' Verify that we can configure the logs to go to stdout. + ''' + + def __init__(self): + + log_data = { + 'diags': 'stdout', + 'error': 'stdout', + 'manager': 'stdout' + } + super().__init__('specify logs to go to stdout', log_data) + + # Configure custom names for manager.log, etc. + self.ts.Disk.records_config.update({ + 'proxy.node.config.manager_log_filename': 'stdout', + 'proxy.config.diags.logfile.filename': 'stdout', + 'proxy.config.error.logfile.filename': 'stdout', + }) + + self.configure_named_custom_log('stdout') + + # The diags.log file will not be created since we are piping to stdout. + # Therefore, simply wait upon the port being open. + self.ts.Ready = When.PortOpen(self.ts.Variables.port) + self.set_log_expectations() + + +class stderrTest(LogFilenamesTest): + ''' + Verify that we can configure the logs to go to stderr. + ''' + + def __init__(self): + + log_data = { + 'diags': 'stderr', + 'error': 'stderr', + 'manager': 'stderr' + } + super().__init__('specify logs to go to stderr', log_data) + + # Configure custom names for manager.log, etc. + self.ts.Disk.records_config.update({ + 'proxy.node.config.manager_log_filename': 'stderr', + 'proxy.config.diags.logfile.filename': 'stderr', + 'proxy.config.error.logfile.filename': 'stderr', + }) + + self.configure_named_custom_log('stderr') + + # The diags.log file will not be created since we are piping to stderr. + # Therefore, simply wait upon the port being open. + self.ts.Ready = When.PortOpen(self.ts.Variables.port) + self.set_log_expectations() + + +# +# Run the tests. +# +DefaultNamedTest() +CustomNamedTest() +stdoutTest() + +# The following stderr test can be run successfully by hand using the replay +# files from the sandbox. All the expected output goes to stderr. However, for +# some reason during the AuTest run, the stderr output stops emitting after the +# logging.yaml file is parsed. This is left here for now because it is valuable +# for use during development, but it is left commented out so that it doesn't +# produce the false failure in CI and developer test runs. +# stderrTest()