Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to create logger with specific name? #204

Closed
heckad opened this issue Jan 17, 2020 · 17 comments
Closed

How to create logger with specific name? #204

heckad opened this issue Jan 17, 2020 · 17 comments
Labels
question Further information is requested

Comments

@heckad
Copy link

heckad commented Jan 17, 2020

I want to create a particular logger for logging events. How to do it?

@Delgan
Copy link
Owner

Delgan commented Jan 17, 2020

Loguru only uses one global "anonymous" logger object. You just import it and use it from anywhere in your program.

It still possible to bind() a name to identify log messages coming from a precise logger:

from loguru import logger

def sink(message):
    record = message.record
    if record.get("name") == "your_named_logger":
        print("Log comes from your named logger")

logger = logger.bind(name="your_named_logger")

logger.info("A logger message")

@Delgan Delgan added the question Further information is requested label Jan 17, 2020
@heckad
Copy link
Author

heckad commented Jan 17, 2020

Thanks for the quick response.

If to write this

import logging

from loguru import logger


class PropagateHandler(logging.Handler):
    def emit(self, record):
        logging.getLogger(record.name).handle(record)


logger.add(PropagateHandler(), format="{message}")

log = logger.bind(name='event log')
log.info("Hi")

then will be error KeyError: "Attempt to overwrite 'name' in LogRecord" What's wrong with this code?

@heckad
Copy link
Author

heckad commented Jan 17, 2020

Also, why add apply to all loggers?
For example

from loguru import logger

logger.remove()


def sink(message):
    print(message, end='')


log = logger.bind(name="your_named_logger")
log.add(sink)
log.info("Simple first message")

logger.info('Simple second message')

Why print twice?

@Delgan
Copy link
Owner

Delgan commented Jan 17, 2020

then will be error KeyError: "Attempt to overwrite 'name' in LogRecord" What's wrong with this code?

This happens when Loguru is used with a Handler from the standard logging library, as the PropagateHandler() in your case. Some keywords are forbidden by Python, see here: logging/__init__.py#L1548-L1552.

If you plan to use loguru and logging at the same time, you should avoid using .bind(name="...") but rather use .bind(logger_name="...") for example. :/

Also, why add apply to all loggers?

It doesn't, all loggers share the same set of handlers. It should not be printed twice. Actually, when I tried your code on my computer, it was not printed twice, so there is probably a typo somewhere else in your code.

@heckad
Copy link
Author

heckad commented Jan 17, 2020

It doesn't, all loggers share the same set of handlers. It should not be printed twice. Actually, when I tried your code on my computer, it was not printed twice, so there is probably a typo somewhere else in your code.

Very interesting.

Снимок

I use python 3.8.1

@Delgan
Copy link
Owner

Delgan commented Jan 17, 2020

@heckad Oh, ok sorry, so this is the expected behavior then. I thought you was saying the same message was printed twice (which would have resulted in four messages overall).

So, as I said, all loggers share the same set of handlers. This means when you call .info() on any logger object, it will propagate the message to all registered handlers, no matter where .add() was called. Doing so eases the process of logging, as you don't have to repeatedly create new logger objects neither do you have to manage to which logger are attached the handlers.

You can use logger anywhere in your code and be sure that the message will be propagated to all configured handlers. You can customize the handling of such message in your sinks. That's the snippet I first shared. If you want to discard messages from one particular module, you can use a custom filter function based on the bound name.

If you really need to use two different logger objects with independent handlers, you can also use copy.deepcopy(), see the documentation.

@heckad
Copy link
Author

heckad commented Jan 17, 2020

If you plan to use loguru and logging at the same time, you should avoid using .bind(name="...") but rather use .bind(logger_name="...") for example. :/

Maybe add a check on exist in name in extra in _log function on 1749 line and if no name then execute previous flow? What do you think?

@Delgan
Copy link
Owner

Delgan commented Jan 17, 2020

Maybe add a check on exist in name in extra in _log function on 1749 line and if no name then execute previous flow? What do you think?

Using name in the extra dict is perfectly valid in the context of Loguru, the limitation is caused by the standard logging. I don't want to propagate this restriction to Loguru, especially considering there is some others attributes which are "forbidden".

Maybe I could add a check in the Loguru StandardSink:

def write(self, message):
record = message.record
message = str(message)
exc = record["exception"]
record = logging.getLogger().makeRecord(
record["name"],
record["level"].no,
record["file"].path,
record["line"],
message,
(),
(exc.type, exc.value, exc.traceback) if exc else None,
record["function"],
record["extra"],
)
if exc:
record.exc_text = "\n"
self._handler.handle(record)

However, this will add an extra runtime check each time a message is logged, I'm not sure it's worth it.

@heckad
Copy link
Author

heckad commented Jan 17, 2020

Why not add the ability to customise logger name?

@Delgan
Copy link
Owner

Delgan commented Jan 17, 2020

Because logger don't have names, name is just an attribute of the record dict (it represents the module __name__, which is equivalent to logging.getLogger(__name__) widely used).

It's part of Loguru's design: you only have to take care of your handlers, the logger is just a "dumb" object used to dispatch log messages to your handlers.

@heckad
Copy link
Author

heckad commented Jan 17, 2020

I use logging.getLogger("event") for getting event logger in every needed place. Similar ability in loguru will be grate!

@Delgan
Copy link
Owner

Delgan commented Jan 17, 2020

That's the purpose of logger.bind(). :)

logger.add(sys.stderr)
logger.add("events.log", filter=lambda rec: "event" in rec["extra"])

logger.info("Some message")  # Only logged to stderr
logger.bind(event=True).info("Some event")  # Logged to 'events.log' too

@Delgan
Copy link
Owner

Delgan commented Mar 8, 2020

@heckad Are you ok with the provided workaround? I don't think I should modify API regarding logger names at that point.

@heckad
Copy link
Author

heckad commented Mar 8, 2020

Yes, we create event_logger like logger.bind(event=True) and use it.

@Delgan
Copy link
Owner

Delgan commented Mar 8, 2020

Fine, that's the standard way to use named logger with Loguru. I'm closing this issue, then. 👍

@Delgan Delgan closed this as completed Mar 8, 2020
@Bobronium
Copy link

Bobronium commented Aug 24, 2022

I was trying to figure out the way to change a name of the record, stumbled upon this issue and drowned in frustration, since there's basically no way mentioned here. I was about to monkeypatch loguru._Logger when I discovered logger.patch. I'm surprised it wasn't mentioned here, despite it does exactly what issue is asking.

TL;DR:

from loguru import logger

custom_logger = logger.patch(lambda record: record.update(name="custom.name"))

custom_logger.info("The name is honored")
2022-08-24 06:09:36.561 | INFO     | custom.logger.name:<module>:5 - The name is honored.

To address out of place :<module>:5, use custom formatter:

import sys
from typing import TYPE_CHECKING

from loguru import logger

if TYPE_CHECKING:
    from loguru import Record


def formatter(record: "Record"):
    if record["name"] in sys.modules:
        # this format will only makes sense if name is a module name
        name_fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan>"
    else:
        # fallback format for custom names
        name_fmt = "<cyan>{name}</cyan>"

    return (
        "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
        "<level>{level: <8}</level> | "
        f"{name_fmt} - <level>{{message}}</level>"
        "\n"
    )


logger.remove()
logger.add(sys.stderr, format=formatter)

custom_logger = logger.patch(lambda record: record.update(name="custom.name"))

custom_logger.info("The name is honored")
2022-08-24 06:10:37.694 | INFO     | custom.logger.name - The name is honored.

Why not extra?

Storing a custom name in extra and then choosing it instead of record name in custom formatter is an option, but not an ideal one: if extra is used in format like {extra}, whenever custom name is used it will be displayed there as well.

Real usecase

An InterceptHandler implementation that honors original logger name:

import logging

from loguru import logger

class InterceptHandler(logging.Handler):
    def __init__(self, name: str = None) -> None:
        if name is not None:
            self._logger = logger.patch(lambda record: record.update(name=name))
        else:
            self._logger = logger
        super().__init__()

    def emit(self, record):
        # Get corresponding Loguru level if it exists
        try:
            level = logger.level(record.levelname).name
        except ValueError:
            level = record.levelno

        # Find caller from where originated the logged message
        frame, depth = logging.currentframe(), 2
        while frame.f_code.co_filename == logging.__file__:
            frame = frame.f_back
            depth += 1

        self._logger.opt(depth=depth, exception=record.exc_info).log(
            level, record.getMessage()
        )

def init_logging():
    logging.basicConfig(handlers=[InterceptHandler()], level=0)
    for stdlib_logger, loguru_handler in (
        (logging.getLogger(name), InterceptHandler(name))
        for name in logging.root.manager.loggerDict
    ):
        stdlib_logger.handlers = [loguru_handler]
        stdlib_logger.propagate = False


init_logging()

logger.add("access.log", filter=lambda record: record["name"] == "uvicorn.access")

Can we make this easier?

It's a shame that setting up unified logging while retaining original loggers names is such a non-trivial task that requires lots of digging and figuring out luguru and logging quirks. Especially when loguru promises to make logging (stupidly) simple.

I think some version of code above should be included in the library and available just via loguru.intercept() so nobody would have to figure out yet another way to patch standard logging.

That's for another issue, though.

@Delgan
Copy link
Owner

Delgan commented Aug 24, 2022

@Bobronium I'm thinking about a way to simplify interfacing with standard logging but it's not trivial. Thanks for your input, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants