We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Persist log file
When running the cuber, there usually is a lot of output (overflowing terminal buffers). It would be great if all of that output would be persisted in a log file as well to allow to easily scan and search through it, afterwards.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `webknossos/webknossos/utils.py`
Content:
```
1 import argparse
2 import calendar
3 import functools
4 import json
5 import logging
6 import os
7 import time
8 from concurrent.futures import as_completed
9 from concurrent.futures._base import Future
10 from datetime import datetime
11 from multiprocessing import cpu_count
12 from pathlib import Path
13 from typing import Any, Callable, Iterable, List, Optional, Union
14 
15 import rich
16 from cluster_tools import WrappedProcessPoolExecutor, get_executor
17 from cluster_tools.schedulers.cluster_executor import ClusterExecutor
18 from rich.progress import Progress
19 
20 times = {}
21 
22 
23 def time_start(identifier: str) -> None:
24     times[identifier] = time.time()
25     logging.debug("{} started".format(identifier))
26 
27 
28 def time_stop(identifier: str) -> None:
29     _time = times.pop(identifier)
30     logging.debug("{} took {:.8f}s".format(identifier, time.time() - _time))
31 
32 
33 def get_executor_for_args(
34     args: Optional[argparse.Namespace],
35 ) -> Union[ClusterExecutor, WrappedProcessPoolExecutor]:
36     executor = None
37     if args is None:
38         # For backwards compatibility with code from other packages
39         # we allow args to be None. In this case we are defaulting
40         # to these values:
41         jobs = cpu_count()
42         executor = get_executor("multiprocessing", max_workers=jobs)
43         logging.info("Using pool of {} workers.".format(jobs))
44     elif args.distribution_strategy == "multiprocessing":
45         # Also accept "processes" instead of job to be compatible with segmentation-tools.
46         # In the long run, the args should be unified and provided by the clustertools.
47         if "jobs" in args:
48             jobs = args.jobs
49         elif "processes" in args:
50             jobs = args.processes
51         else:
52             jobs = cpu_count()
53 
54         executor = get_executor("multiprocessing", max_workers=jobs)
55         logging.info("Using pool of {} workers.".format(jobs))
56     elif args.distribution_strategy in ("slurm", "kubernetes"):
57         if args.job_resources is None:
58             resources_example = (
59                 '{"mem": "1G"}'
60                 if args.distribution_strategy == "slurm"
61                 else '{"memory": "1G"}'
62             )
63             raise argparse.ArgumentTypeError(
64                 f"Job resources (--job_resources) has to be provided when using {args.distribution_strategy} as distribution strategy. Example: --job_resources='{resources_example}'"
65             )
66 
67         executor = get_executor(
68             args.distribution_strategy,
69             debug=True,
70             keep_logs=True,
71             job_resources=json.loads(args.job_resources),
72         )
73         logging.info(f"Using {args.distribution_strategy} cluster.")
74     else:
75         logging.error(
76             "Unknown distribution strategy: {}".format(args.distribution_strategy)
77         )
78 
79     return executor
80 
81 
82 F = Callable[..., Any]
83 
84 
85 def named_partial(func: F, *args: Any, **kwargs: Any) -> F:
86     # Propagate __name__ and __doc__ attributes to partial function
87     partial_func = functools.partial(func, *args, **kwargs)
88     functools.update_wrapper(partial_func, func)
89     if hasattr(func, "__annotations__"):
90         # Generic types cannot be pickled in Python <= 3.6, see https://github.com/python/typing/issues/511
91         partial_func.__annotations__ = {}
92     return partial_func
93 
94 
95 def wait_and_ensure_success(
96     futures: List[Future], progress_desc: Optional[str] = None
97 ) -> List[Any]:
98     """Waits for all futures to complete and raises an exception
99     as soon as a future resolves with an error."""
100 
101     results = []
102     if progress_desc is None:
103         for fut in as_completed(futures):
104             results.append(fut.result())
105     else:
106         with get_rich_progress() as progress:
107             task = progress.add_task(progress_desc, total=len(futures))
108             for fut in as_completed(futures):
109                 results.append(fut.result())
110                 progress.update(task, advance=1)
111     return results
112 
113 
114 def snake_to_camel_case(snake_case_name: str) -> str:
115     parts = snake_case_name.split("_")
116     return parts[0] + "".join(part.title() for part in parts[1:])
117 
118 
119 def get_chunks(arr: List[Any], chunk_size: int) -> Iterable[List[Any]]:
120     for i in range(0, len(arr), chunk_size):
121         yield arr[i : i + chunk_size]
122 
123 
124 def time_since_epoch_in_ms() -> int:
125     d = datetime.utcnow()
126     unixtime = calendar.timegm(d.utctimetuple())
127     return unixtime * 1000
128 
129 
130 def copy_directory_with_symlinks(
131     src_path: Path,
132     dst_path: Path,
133     ignore: Iterable[str] = tuple(),
134     make_relative: bool = False,
135 ) -> None:
136     """
137     Links all directories in src_path / dir_name to dst_path / dir_name.
138     """
139     for item in src_path.iterdir():
140         if item.name not in ignore:
141             symlink_path = dst_path / item.name
142             if make_relative:
143                 rel_or_abspath = Path(os.path.relpath(item, symlink_path.parent))
144             else:
145                 rel_or_abspath = item.resolve()
146             symlink_path.symlink_to(rel_or_abspath)
147 
148 
149 def setup_logging(args: argparse.Namespace) -> None:
150     logging.basicConfig(
151         level=(logging.DEBUG if args.verbose else logging.INFO),
152         format="%(asctime)s %(levelname)s %(message)s",
153     )
154 
155 
156 def add_verbose_flag(parser: argparse.ArgumentParser) -> None:
157     parser.add_argument(
158         "--verbose", help="Verbose output", dest="verbose", action="store_true"
159     )
160 
161     parser.set_defaults(verbose=False)
162 
163 
164 def get_rich_progress() -> Progress:
165     return Progress(
166         "[progress.description]{task.description}",
167         rich.progress.BarColumn(),
168         "[progress.percentage]{task.percentage:>3.0f}%",
169         rich.progress.TimeElapsedColumn(),
170         "|",
171         rich.progress.TimeRemainingColumn(),
172     )
173
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/webknossos/webknossos/utils.py b/webknossos/webknossos/utils.py
--- a/webknossos/webknossos/utils.py
+++ b/webknossos/webknossos/utils.py
@@ -4,6 +4,7 @@
 import json
 import logging
 import os
+import sys
 import time
 from concurrent.futures import as_completed
 from concurrent.futures._base import Future
@@ -147,10 +148,28 @@
 
 
 def setup_logging(args: argparse.Namespace) -> None:
-    logging.basicConfig(
-        level=(logging.DEBUG if args.verbose else logging.INFO),
-        format="%(asctime)s %(levelname)s %(message)s",
-    )
+    log_path = Path(f"./logs/cuber_{time.strftime('%Y-%m-%d_%H%M%S')}.txt")
+
+    console_log_level = logging.DEBUG if args.verbose else logging.INFO
+    file_log_level = logging.DEBUG
+
+    logging_formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
+
+    # Always set the global log level to the more verbose of console_log_level and
+    # file_log_level to allow to log with different log levels to console and files.
+    root_logger = logging.getLogger()
+    root_logger.setLevel(min(console_log_level, file_log_level))
+
+    console = logging.StreamHandler(sys.stdout)
+    console.setLevel(console_log_level)
+    console.setFormatter(logging_formatter)
+    root_logger.addHandler(console)
+
+    log_path.parent.mkdir(parents=True, exist_ok=True)
+    file_handler = logging.FileHandler(log_path, mode="w", encoding="UTF-8")
+    file_handler.setLevel(file_log_level)
+    file_handler.setFormatter(logging_formatter)
+    root_logger.addHandler(file_handler)
 
 
 def add_verbose_flag(parser: argparse.ArgumentParser) -> None:

{"golden_diff": "diff --git a/webknossos/webknossos/utils.py b/webknossos/webknossos/utils.py\n--- a/webknossos/webknossos/utils.py\n+++ b/webknossos/webknossos/utils.py\n@@ -4,6 +4,7 @@\n import json\n import logging\n import os\n+import sys\n import time\n from concurrent.futures import as_completed\n from concurrent.futures._base import Future\n@@ -147,10 +148,28 @@\n \n \n def setup_logging(args: argparse.Namespace) -> None:\n-    logging.basicConfig(\n-        level=(logging.DEBUG if args.verbose else logging.INFO),\n-        format=\"%(asctime)s %(levelname)s %(message)s\",\n-    )\n+    log_path = Path(f\"./logs/cuber_{time.strftime('%Y-%m-%d_%H%M%S')}.txt\")\n+\n+    console_log_level = logging.DEBUG if args.verbose else logging.INFO\n+    file_log_level = logging.DEBUG\n+\n+    logging_formatter = logging.Formatter(\"%(asctime)s %(levelname)s %(message)s\")\n+\n+    # Always set the global log level to the more verbose of console_log_level and\n+    # file_log_level to allow to log with different log levels to console and files.\n+    root_logger = logging.getLogger()\n+    root_logger.setLevel(min(console_log_level, file_log_level))\n+\n+    console = logging.StreamHandler(sys.stdout)\n+    console.setLevel(console_log_level)\n+    console.setFormatter(logging_formatter)\n+    root_logger.addHandler(console)\n+\n+    log_path.parent.mkdir(parents=True, exist_ok=True)\n+    file_handler = logging.FileHandler(log_path, mode=\"w\", encoding=\"UTF-8\")\n+    file_handler.setLevel(file_log_level)\n+    file_handler.setFormatter(logging_formatter)\n+    root_logger.addHandler(file_handler)\n \n \n def add_verbose_flag(parser: argparse.ArgumentParser) -> None:\n", "issue": "Persist log file\nWhen running the cuber, there usually is a lot of output (overflowing terminal buffers). It would be great if all of that output would be persisted in a log file as well to allow to easily scan and search through it, afterwards.\n", "before_files": [{"content": "import argparse\nimport calendar\nimport functools\nimport json\nimport logging\nimport os\nimport time\nfrom concurrent.futures import as_completed\nfrom concurrent.futures._base import Future\nfrom datetime import datetime\nfrom multiprocessing import cpu_count\nfrom pathlib import Path\nfrom typing import Any, Callable, Iterable, List, Optional, Union\n\nimport rich\nfrom cluster_tools import WrappedProcessPoolExecutor, get_executor\nfrom cluster_tools.schedulers.cluster_executor import ClusterExecutor\nfrom rich.progress import Progress\n\ntimes = {}\n\n\ndef time_start(identifier: str) -> None:\n    times[identifier] = time.time()\n    logging.debug(\"{} started\".format(identifier))\n\n\ndef time_stop(identifier: str) -> None:\n    _time = times.pop(identifier)\n    logging.debug(\"{} took {:.8f}s\".format(identifier, time.time() - _time))\n\n\ndef get_executor_for_args(\n    args: Optional[argparse.Namespace],\n) -> Union[ClusterExecutor, WrappedProcessPoolExecutor]:\n    executor = None\n    if args is None:\n        # For backwards compatibility with code from other packages\n        # we allow args to be None. In this case we are defaulting\n        # to these values:\n        jobs = cpu_count()\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy == \"multiprocessing\":\n        # Also accept \"processes\" instead of job to be compatible with segmentation-tools.\n        # In the long run, the args should be unified and provided by the clustertools.\n        if \"jobs\" in args:\n            jobs = args.jobs\n        elif \"processes\" in args:\n            jobs = args.processes\n        else:\n            jobs = cpu_count()\n\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy in (\"slurm\", \"kubernetes\"):\n        if args.job_resources is None:\n            resources_example = (\n                '{\"mem\": \"1G\"}'\n                if args.distribution_strategy == \"slurm\"\n                else '{\"memory\": \"1G\"}'\n            )\n            raise argparse.ArgumentTypeError(\n                f\"Job resources (--job_resources) has to be provided when using {args.distribution_strategy} as distribution strategy. Example: --job_resources='{resources_example}'\"\n            )\n\n        executor = get_executor(\n            args.distribution_strategy,\n            debug=True,\n            keep_logs=True,\n            job_resources=json.loads(args.job_resources),\n        )\n        logging.info(f\"Using {args.distribution_strategy} cluster.\")\n    else:\n        logging.error(\n            \"Unknown distribution strategy: {}\".format(args.distribution_strategy)\n        )\n\n    return executor\n\n\nF = Callable[..., Any]\n\n\ndef named_partial(func: F, *args: Any, **kwargs: Any) -> F:\n    # Propagate __name__ and __doc__ attributes to partial function\n    partial_func = functools.partial(func, *args, **kwargs)\n    functools.update_wrapper(partial_func, func)\n    if hasattr(func, \"__annotations__\"):\n        # Generic types cannot be pickled in Python <= 3.6, see https://github.com/python/typing/issues/511\n        partial_func.__annotations__ = {}\n    return partial_func\n\n\ndef wait_and_ensure_success(\n    futures: List[Future], progress_desc: Optional[str] = None\n) -> List[Any]:\n    \"\"\"Waits for all futures to complete and raises an exception\n    as soon as a future resolves with an error.\"\"\"\n\n    results = []\n    if progress_desc is None:\n        for fut in as_completed(futures):\n            results.append(fut.result())\n    else:\n        with get_rich_progress() as progress:\n            task = progress.add_task(progress_desc, total=len(futures))\n            for fut in as_completed(futures):\n                results.append(fut.result())\n                progress.update(task, advance=1)\n    return results\n\n\ndef snake_to_camel_case(snake_case_name: str) -> str:\n    parts = snake_case_name.split(\"_\")\n    return parts[0] + \"\".join(part.title() for part in parts[1:])\n\n\ndef get_chunks(arr: List[Any], chunk_size: int) -> Iterable[List[Any]]:\n    for i in range(0, len(arr), chunk_size):\n        yield arr[i : i + chunk_size]\n\n\ndef time_since_epoch_in_ms() -> int:\n    d = datetime.utcnow()\n    unixtime = calendar.timegm(d.utctimetuple())\n    return unixtime * 1000\n\n\ndef copy_directory_with_symlinks(\n    src_path: Path,\n    dst_path: Path,\n    ignore: Iterable[str] = tuple(),\n    make_relative: bool = False,\n) -> None:\n    \"\"\"\n    Links all directories in src_path / dir_name to dst_path / dir_name.\n    \"\"\"\n    for item in src_path.iterdir():\n        if item.name not in ignore:\n            symlink_path = dst_path / item.name\n            if make_relative:\n                rel_or_abspath = Path(os.path.relpath(item, symlink_path.parent))\n            else:\n                rel_or_abspath = item.resolve()\n            symlink_path.symlink_to(rel_or_abspath)\n\n\ndef setup_logging(args: argparse.Namespace) -> None:\n    logging.basicConfig(\n        level=(logging.DEBUG if args.verbose else logging.INFO),\n        format=\"%(asctime)s %(levelname)s %(message)s\",\n    )\n\n\ndef add_verbose_flag(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\n        \"--verbose\", help=\"Verbose output\", dest=\"verbose\", action=\"store_true\"\n    )\n\n    parser.set_defaults(verbose=False)\n\n\ndef get_rich_progress() -> Progress:\n    return Progress(\n        \"[progress.description]{task.description}\",\n        rich.progress.BarColumn(),\n        \"[progress.percentage]{task.percentage:>3.0f}%\",\n        rich.progress.TimeElapsedColumn(),\n        \"|\",\n        rich.progress.TimeRemainingColumn(),\n    )\n", "path": "webknossos/webknossos/utils.py"}], "after_files": [{"content": "import argparse\nimport calendar\nimport functools\nimport json\nimport logging\nimport os\nimport sys\nimport time\nfrom concurrent.futures import as_completed\nfrom concurrent.futures._base import Future\nfrom datetime import datetime\nfrom multiprocessing import cpu_count\nfrom pathlib import Path\nfrom typing import Any, Callable, Iterable, List, Optional, Union\n\nimport rich\nfrom cluster_tools import WrappedProcessPoolExecutor, get_executor\nfrom cluster_tools.schedulers.cluster_executor import ClusterExecutor\nfrom rich.progress import Progress\n\ntimes = {}\n\n\ndef time_start(identifier: str) -> None:\n    times[identifier] = time.time()\n    logging.debug(\"{} started\".format(identifier))\n\n\ndef time_stop(identifier: str) -> None:\n    _time = times.pop(identifier)\n    logging.debug(\"{} took {:.8f}s\".format(identifier, time.time() - _time))\n\n\ndef get_executor_for_args(\n    args: Optional[argparse.Namespace],\n) -> Union[ClusterExecutor, WrappedProcessPoolExecutor]:\n    executor = None\n    if args is None:\n        # For backwards compatibility with code from other packages\n        # we allow args to be None. In this case we are defaulting\n        # to these values:\n        jobs = cpu_count()\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy == \"multiprocessing\":\n        # Also accept \"processes\" instead of job to be compatible with segmentation-tools.\n        # In the long run, the args should be unified and provided by the clustertools.\n        if \"jobs\" in args:\n            jobs = args.jobs\n        elif \"processes\" in args:\n            jobs = args.processes\n        else:\n            jobs = cpu_count()\n\n        executor = get_executor(\"multiprocessing\", max_workers=jobs)\n        logging.info(\"Using pool of {} workers.\".format(jobs))\n    elif args.distribution_strategy in (\"slurm\", \"kubernetes\"):\n        if args.job_resources is None:\n            resources_example = (\n                '{\"mem\": \"1G\"}'\n                if args.distribution_strategy == \"slurm\"\n                else '{\"memory\": \"1G\"}'\n            )\n            raise argparse.ArgumentTypeError(\n                f\"Job resources (--job_resources) has to be provided when using {args.distribution_strategy} as distribution strategy. Example: --job_resources='{resources_example}'\"\n            )\n\n        executor = get_executor(\n            args.distribution_strategy,\n            debug=True,\n            keep_logs=True,\n            job_resources=json.loads(args.job_resources),\n        )\n        logging.info(f\"Using {args.distribution_strategy} cluster.\")\n    else:\n        logging.error(\n            \"Unknown distribution strategy: {}\".format(args.distribution_strategy)\n        )\n\n    return executor\n\n\nF = Callable[..., Any]\n\n\ndef named_partial(func: F, *args: Any, **kwargs: Any) -> F:\n    # Propagate __name__ and __doc__ attributes to partial function\n    partial_func = functools.partial(func, *args, **kwargs)\n    functools.update_wrapper(partial_func, func)\n    if hasattr(func, \"__annotations__\"):\n        # Generic types cannot be pickled in Python <= 3.6, see https://github.com/python/typing/issues/511\n        partial_func.__annotations__ = {}\n    return partial_func\n\n\ndef wait_and_ensure_success(\n    futures: List[Future], progress_desc: Optional[str] = None\n) -> List[Any]:\n    \"\"\"Waits for all futures to complete and raises an exception\n    as soon as a future resolves with an error.\"\"\"\n\n    results = []\n    if progress_desc is None:\n        for fut in as_completed(futures):\n            results.append(fut.result())\n    else:\n        with get_rich_progress() as progress:\n            task = progress.add_task(progress_desc, total=len(futures))\n            for fut in as_completed(futures):\n                results.append(fut.result())\n                progress.update(task, advance=1)\n    return results\n\n\ndef snake_to_camel_case(snake_case_name: str) -> str:\n    parts = snake_case_name.split(\"_\")\n    return parts[0] + \"\".join(part.title() for part in parts[1:])\n\n\ndef get_chunks(arr: List[Any], chunk_size: int) -> Iterable[List[Any]]:\n    for i in range(0, len(arr), chunk_size):\n        yield arr[i : i + chunk_size]\n\n\ndef time_since_epoch_in_ms() -> int:\n    d = datetime.utcnow()\n    unixtime = calendar.timegm(d.utctimetuple())\n    return unixtime * 1000\n\n\ndef copy_directory_with_symlinks(\n    src_path: Path,\n    dst_path: Path,\n    ignore: Iterable[str] = tuple(),\n    make_relative: bool = False,\n) -> None:\n    \"\"\"\n    Links all directories in src_path / dir_name to dst_path / dir_name.\n    \"\"\"\n    for item in src_path.iterdir():\n        if item.name not in ignore:\n            symlink_path = dst_path / item.name\n            if make_relative:\n                rel_or_abspath = Path(os.path.relpath(item, symlink_path.parent))\n            else:\n                rel_or_abspath = item.resolve()\n            symlink_path.symlink_to(rel_or_abspath)\n\n\ndef setup_logging(args: argparse.Namespace) -> None:\n    log_path = Path(f\"./logs/cuber_{time.strftime('%Y-%m-%d_%H%M%S')}.txt\")\n\n    console_log_level = logging.DEBUG if args.verbose else logging.INFO\n    file_log_level = logging.DEBUG\n\n    logging_formatter = logging.Formatter(\"%(asctime)s %(levelname)s %(message)s\")\n\n    # Always set the global log level to the more verbose of console_log_level and\n    # file_log_level to allow to log with different log levels to console and files.\n    root_logger = logging.getLogger()\n    root_logger.setLevel(min(console_log_level, file_log_level))\n\n    console = logging.StreamHandler(sys.stdout)\n    console.setLevel(console_log_level)\n    console.setFormatter(logging_formatter)\n    root_logger.addHandler(console)\n\n    log_path.parent.mkdir(parents=True, exist_ok=True)\n    file_handler = logging.FileHandler(log_path, mode=\"w\", encoding=\"UTF-8\")\n    file_handler.setLevel(file_log_level)\n    file_handler.setFormatter(logging_formatter)\n    root_logger.addHandler(file_handler)\n\n\ndef add_verbose_flag(parser: argparse.ArgumentParser) -> None:\n    parser.add_argument(\n        \"--verbose\", help=\"Verbose output\", dest=\"verbose\", action=\"store_true\"\n    )\n\n    parser.set_defaults(verbose=False)\n\n\ndef get_rich_progress() -> Progress:\n    return Progress(\n        \"[progress.description]{task.description}\",\n        rich.progress.BarColumn(),\n        \"[progress.percentage]{task.percentage:>3.0f}%\",\n        rich.progress.TimeElapsedColumn(),\n        \"|\",\n        rich.progress.TimeRemainingColumn(),\n    )\n", "path": "webknossos/webknossos/utils.py"}]}