diff --git a/_appmap/env.py b/_appmap/env.py index ad1f7290..b74683bc 100644 --- a/_appmap/env.py +++ b/_appmap/env.py @@ -37,6 +37,7 @@ def __init__(self, env=None, cwd=None): else: self._env.pop(k, None) + self.log_file_creation_failed = False self._configure_logging() enabled = self._env.get("_APPMAP", "false") self._enabled = enabled is None or enabled.lower() != "false" @@ -133,6 +134,21 @@ def display_params(self): def getLogger(self, name) -> trace_logger.TraceLogger: return cast(trace_logger.TraceLogger, logging.getLogger(name)) + def determine_log_file(self): + log_file = "appmap.log" + + # Try creating the log file in the current directory + try: + with open(log_file, 'a', encoding='UTF8'): + pass + except IOError: + # The circumstances in which creation is going to fail + # are also those in which the user doesn't care whether + # there's a log file (e.g. when starting a REPL). + return None + return log_file + + def _configure_logging(self): trace_logger.install() @@ -179,13 +195,19 @@ def _configure_logging(self): log_level = self.get("APPMAP_LOG_LEVEL", "info").upper() loggers = config_dict["loggers"] loggers["appmap"]["level"] = loggers["_appmap"]["level"] = log_level + + log_file = self.determine_log_file() + # Use NullHandler if log_file is None to avoid complicating the configuration + # with the absence of the "default" handler. config_dict["handlers"] = { "default": { "class": "logging.handlers.RotatingFileHandler", "formatter": "default", - "filename": "appmap.log", + "filename": log_file, "maxBytes": 50 * 1024 * 1024, "backupCount": 1, + } if log_file is not None else { + "class": "logging.NullHandler" }, "stderr": { "class": "logging.StreamHandler", @@ -194,6 +216,7 @@ def _configure_logging(self): "stream": "ext://sys.stderr", }, } + self.log_file_creation_failed = log_file is None if log_config is not None: name, level = log_config.split("=", 2) @@ -213,3 +236,7 @@ def initialize(**kwargs): Env.reset(**kwargs) logger = logging.getLogger(__name__) logger.info("appmap enabled: %s", Env.current.enabled) + if Env.current.log_file_creation_failed: + # Writing to stderr makes the REPL fail in vscode-python. + # https://github.com/microsoft/vscode-python/blob/c71c85ebf3749d5fac76899feefb21ee321a4b5b/src/client/common/process/rawProcessApis.ts#L268-L269 + logger.info("appmap.log cannot be created") diff --git a/ci/readonly-mount-appmap.log b/ci/readonly-mount-appmap.log new file mode 100644 index 00000000..249845e3 --- /dev/null +++ b/ci/readonly-mount-appmap.log @@ -0,0 +1 @@ +# For a test in smoketest \ No newline at end of file diff --git a/ci/run_tests.sh b/ci/run_tests.sh index d97299a0..c78d5415 100755 --- a/ci/run_tests.sh +++ b/ci/run_tests.sh @@ -6,4 +6,5 @@ docker run -q -i${t} --rm\ -v $PWD/dist:/dist -v $PWD/_appmap/test/data/unittest:/_appmap/test/data/unittest\ -v $PWD/ci:/ci\ -w /tmp\ + -v $PWD/ci/readonly-mount-appmap.log:/tmp/appmap.log:ro\ python:3.11 bash -ce "${@:-/ci/smoketest.sh; /ci/test_pipenv.sh; /ci/test_poetry.sh}" diff --git a/ci/smoketest.sh b/ci/smoketest.sh index f719533f..3d0eeb13 100755 --- a/ci/smoketest.sh +++ b/ci/smoketest.sh @@ -19,6 +19,22 @@ EOF fi } +test_log_file_not_writable() +{ + cat < test_log_file_not_writable.py +import appmap +EOF + + python test_log_file_not_writable.py + + if [[ $? -eq 0 ]]; then + echo 'Script executed successfully' + else + echo 'Script execution failed' + exit 1 + fi +} + set -ex pip -q install -U pip pytest "flask>=2,<3" python-decouple pip -q install /dist/appmap-*-py3-none-any.whl @@ -48,4 +64,6 @@ else echo 'No appmap generated?' find $PWD exit 1 -fi \ No newline at end of file +fi + +test_log_file_not_writable \ No newline at end of file