Repository: huggingface/accelerate

Issue:
Logging does not respect the "%(lineno)s" and "%(filename)s".
### System Info

```Shell
Copy-and-paste the text below in your GitHub issue

- `Accelerate` version: 0.24.1
- Platform: Linux-6.2.0-35-generic-x86_64-with-glibc2.35
- Python version: 3.11.5
- Numpy version: 1.26.1
- PyTorch version (GPU?): 2.1.0+cu121 (True)
- PyTorch XPU available: False
- PyTorch NPU available: False
- System RAM: 125.48 GB
- GPU type: NVIDIA GeForce RTX 3090
- `Accelerate` default config:
        - compute_environment: LOCAL_MACHINE
        - distributed_type: NO
        - mixed_precision: bf16
        - use_cpu: False
        - debug: False
        - num_processes: 1
        - machine_rank: 0
        - num_machines: 1
        - gpu_ids: all
        - rdzv_backend: static
        - same_network: True
        - main_training_function: main
        - downcast_bf16: no
        - tpu_use_cluster: False
        - tpu_use_sudo: False
        - tpu_env: []
```


### Information

- [ ] The official example scripts
- [X] My own modified scripts

### Tasks

- [ ] One of the scripts in the examples/ folder of Accelerate or an officially supported `no_trainer` script in the `examples` folder of the `transformers` repo (such as `run_no_trainer_glue.py`)
- [X] My own task or dataset (give details below)

### Reproduction

When using `accelerate.logging.get_logger` to log, all of the logs says it came from `logging.py` at line 60.

Interestingly, it respects the current module's name correctly.

```python
import logging

from accelerate.logging import get_logger
from rich.logging import RichHandler

logger = get_logger(__name__)

if __name__ == "__main__":
    logging.basicConfig(
        format="%(name)s: %(lineno)s - %(message)s",
        datefmt="%m/%d %H:%M:%S",
        level=logging.INFO,
        handlers=[RichHandler(show_time=True, show_level=True, show_path=True)],
    )
    logger.info("Test")
```

The output looks something like this.

```
11/02 16:45:19 INFO     __main__: 60 - Test                                                logging.py:60
```

### Expected behavior

The line number and filename should respect where the `logger.info()` is called.


Discussion:
I think the issue is with the order in which the logger settings are set. I modified the code like this and it worked for me:

```python
import logging
from accelerate import Accelerator
from accelerate.logging import get_logger
from rich.logging import RichHandler

accelerator = Accelerator()

my_logger = logging.getLogger(__name__)
my_logger.setLevel(logging.INFO)
formatter = logging.Formatter(fmt="%(name)s: %(lineno)s - %(message)s", datefmt="%m/%d %H:%M:%S")
handler = RichHandler(show_time=True, show_level=True, show_path=True)
handler.setFormatter(formatter)
my_logger.addHandler(handler)

logger = get_logger(__name__)
logger.logger = my_logger
logger.info("Test")
```
Thank you, @BenjaminBossan 

However, the following code still shows `logging.py:60` for me.  I'm using accelerate==0.24.1, rich==13.6.0.
That's strange. I put the code in a new file and when I run it, I get:

> `11/03 12:18:28 INFO     __main__: 60 - Test`

where `__main__` is the `__name__` passed to `my_logger = logging.getLogger(__name__)`.
Yes, the line number 60 is wrong. It's from logging.py
Oh I see now, thanks.

The issue is that accelerate takes control of the logging call itself, which results in the trace pointing to the line in `logging.py` that calls the log. I read a bit in the Python docs and I think there is no available solution here. We could probably implement the `main_process_only` part using filters, which would avoid this problem. But the `in_order` part doesn't appear like it can be implemented without taking full control of the log call, which will result in the issue. Do you have any idea @muellerzr?

If the latter is a feature you can live without, a solution for you could be to create your own logger instance and use it for your custom logs, which should then show the correct line number.
Instead of defining a new logger class, maybe there's a way to use a handler and use the original Python getLogger? I'm not sure about how it can be addressed, but actually for me I rarely use `in_order` so if there's a fix for `main_process_only` then it would be cool! In the worst case, I can `gather` the information from all processes and print on the main process.
Perhaps the `in_order`-part could be implemented by a `Handler` by implementing it in `emit`, but I would expect that the same problem of the incorrect line number being shown would still apply. Not 100% sure here.
This issue has been automatically marked as stale because it has not had recent activity. If you think this still needs to be addressed please comment on this thread.

Please note that issues that do not follow the [contributing guidelines](https://github.com/huggingface/accelerate/blob/main/CONTRIBUTING.md) are likely to be ignored.

Resolving patch:
diff --git a/src/accelerate/logging.py b/src/accelerate/logging.py
--- a/src/accelerate/logging.py
+++ b/src/accelerate/logging.py
@@ -54,6 +54,8 @@ def log(self, level, msg, *args, **kwargs):
             )
         main_process_only = kwargs.pop("main_process_only", True)
         in_order = kwargs.pop("in_order", False)
+        # set `stacklevel` to exclude ourself in `Logger.findCaller()` while respecting user's choice
+        kwargs.setdefault("stacklevel", 2)
 
         if self.isEnabledFor(level):
             if self._should_log(main_process_only):


Test patch:
diff --git a/tests/test_logging.py b/tests/test_logging.py
new file mode 100644
--- /dev/null
+++ b/tests/test_logging.py
@@ -0,0 +1,91 @@
+# Copyright 2024 The HuggingFace Team. All rights reserved.
+#
+# Licensed 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 inspect
+import logging
+import os
+
+import pytest
+
+from accelerate import Accelerator
+from accelerate.logging import get_logger
+
+
+def current_lineno() -> int:
+    # A simple helper that returns the lineno of its call-site.
+    caller_frame = inspect.currentframe().f_back
+    caller_info = inspect.getframeinfo(caller_frame)
+    return caller_info.lineno
+
+
+class CustomLogger(logging.LoggerAdapter):
+    # Mocks a user-defined custom logger wrapper that sets `stacklevel=3`.
+    def log(self, level, msg, *args, **kwargs):
+        # E.g. the user wants to modify `stacklevel`, `accelerate.logging`
+        # should respect the user's `stacklevel`. For the specific value
+        # of `3`, calling `CustomLogger.log()`, etc., should log that callsite,
+        # rather than the callsite of the following `self.logger.log()`.
+        kwargs["stacklevel"] = 3
+        self.logger.log(level, msg, *args, **kwargs)
+
+
+@pytest.fixture(scope="module")
+def accelerator():
+    return Accelerator()
+
+
+@pytest.mark.usefixtures("accelerator")
+def test_log_stack(caplog):
+    logger = get_logger(__name__)
+    logging.basicConfig(
+        format="%(filename)s:%(name)s:%(lineno)s:%(funcName)s - %(message)s",
+        datefmt="%m/%d %H:%M:%S",
+    )
+
+    message = "Test"
+    lineno = current_lineno() + 1  # the next line is the actual callsite
+    logger.warning(message)
+
+    assert len(caplog.records) == 1
+    rec = caplog.records[0]
+    assert rec.levelname == logging.getLevelName(logging.WARNING)
+    assert rec.filename == os.path.basename(__file__)
+    assert rec.name == __name__
+    assert rec.lineno == lineno
+    assert rec.funcName == test_log_stack.__name__
+    assert rec.message == message
+
+
+@pytest.mark.usefixtures("accelerator")
+def test_custom_stacklevel(caplog):
+    wrapped_logger = get_logger(__name__)
+    logging.basicConfig(
+        format="%(filename)s:%(name)s:%(lineno)s:%(funcName)s - %(message)s",
+        datefmt="%m/%d %H:%M:%S",
+    )
+    logger = CustomLogger(wrapped_logger, {})
+
+    message = "Test"
+    lineno = current_lineno() + 1  # the next line is the actual callsite
+    logger.warning(message)
+
+    # `CustomLogger.log` set custom `stacklevel=3`, so `logger.warning` should
+    # log its callsite (rather than those of the `warpped_logger`).
+    assert len(caplog.records) == 1
+    rec = caplog.records[0]
+    assert rec.levelname == logging.getLevelName(logging.WARNING)
+    assert rec.filename == os.path.basename(__file__)
+    assert rec.name == __name__
+    assert rec.lineno == lineno
+    assert rec.funcName == test_custom_stacklevel.__name__
+    assert rec.message == message