loguru icon indicating copy to clipboard operation
loguru copied to clipboard

Is it possible to use Loguru with OpenTelemetry Logging Instrumentation?

Open luisazevedo-mb opened this issue 3 years ago • 15 comments

I'm trying to configure Loguru with OpenTelemetry Logging Instrumentation, but I'm not getting. I can see that LoggingInstrumentor().instrument() get the default factory of loggin and than add some controls, changing the default factory of logging. I'm not able to find a way to define the factory for Loguru.

Block the code executed by LoggingInstrumentor().instrument()

    def _instrument(self, **kwargs):

        provider = kwargs.get("tracer_provider", None) or get_tracer_provider()
        old_factory = logging.getLogRecordFactory()
        LoggingInstrumentor._old_factory = old_factory

        service_name = None

        def record_factory(*args, **kwargs):
            record = old_factory(*args, **kwargs)

            record.otelSpanID = "0"
            record.otelTraceID = "0"

            nonlocal service_name
            if service_name is None:
                resource = getattr(provider, "resource", None)
                if resource:
                    service_name = (
                        resource.attributes.get("service.name") or ""
                    )
                else:
                    service_name = ""

            record.otelServiceName = service_name

            span = get_current_span()
            if span != INVALID_SPAN:
                ctx = span.get_span_context()
                if ctx != INVALID_SPAN_CONTEXT:
                    record.otelSpanID = format(ctx.span_id, "016x")
                    record.otelTraceID = format(ctx.trace_id, "032x")
            return record

        logging.setLogRecordFactory(record_factory)

        set_logging_format = kwargs.get(
            "set_logging_format",
            environ.get(OTEL_PYTHON_LOG_CORRELATION, "false").lower()
            == "true",
        )

        if set_logging_format:
            log_format = kwargs.get(
                "logging_format", environ.get(OTEL_PYTHON_LOG_FORMAT, None)
            )
            log_format = log_format or DEFAULT_LOGGING_FORMAT

            log_level = kwargs.get(
                "log_level", LEVELS.get(environ.get(OTEL_PYTHON_LOG_LEVEL))
            )
            log_level = log_level or logging.INFO

            logging.basicConfig(format=log_format, level=log_level)

luisazevedo-mb avatar Jul 06 '22 00:07 luisazevedo-mb

Hi @luisazevedo-mb.

I'm not sure it work but what about using InterceptHandler or PropagateHandler (depending on your goal) to integrate Loguru with standard logging module?

Otherwise you may need to re-implement the record_factory() in the snippet you shared in way compatible with Loguru. It's just about retrieving the contextual information and adding them to the extra dict.

Delgan avatar Jul 07 '22 20:07 Delgan

+1 for this.

I was working with NewRelic for work the other day and discovered that NewRelic's auto-instrumentation CLI does support loguru.

Here is the PR in the newrelic/newrelic-python-agent repo where loguru instrumentation was added: https://github.com/newrelic/newrelic-python-agent/pull/552

I think this could be a good reference resource.

phitoduck avatar Jul 27 '22 23:07 phitoduck

This would be a great thing to have on the next release

Corfucinas avatar Oct 04 '22 12:10 Corfucinas

@luisazevedo-mb were you able to integrate Loguru and Open Telemetry? I'm facing a similar issue. I want logs in Loguru format propagate all the way to open telemetry.

elanqo avatar Sep 20 '23 14:09 elanqo

I also need this feature, any plan for this?

axiangcoding avatar Dec 27 '23 07:12 axiangcoding

Apologies to everyone eagerly anticipating this feature, but not being familiar with OpenTelemetry I'm still not sure about what exactly is expected. If someone could please clarify the desired output of the integration between Loguru and OpenTelemetry, perhaps I could craft a working solution.

If I understand correctly, the LoggingInstrumentor() from opentelemetry can be used this way:

import logging

from opentelemetry.instrumentation.logging import LoggingInstrumentor

LoggingInstrumentor().instrument(set_logging_format=True)

logger = logging.getLogger(__name__)

logger.info("This is an info message")

This produces the following logs:

2024-01-02 18:31:27,332 INFO [__main__] [file.py:9] [trace_id=0 span_id=0 resource.service.name= trace_sampled=False] - This is an info message

We can observe some contextual data was automatically added. Is that what you try to propagate to Loguru? If this is the case, we can easily re-implement an "instrumentor" through the patch() method of Loguru. Here is an example based on the original LoggingInstrumentor._instrument() implementation:

from loguru import logger
import sys

from opentelemetry.trace import (
    INVALID_SPAN,
    INVALID_SPAN_CONTEXT,
    get_current_span,
    get_tracer_provider,
)


def instrument_loguru():
    provider = get_tracer_provider()
    service_name = None

    def add_trace_context(record):
        record["extra"]["otelSpanID"] = "0"
        record["extra"]["otelTraceID"] = "0"
        record["extra"]["otelTraceSampled"] = False

        nonlocal service_name
        if service_name is None:
            resource = getattr(provider, "resource", None)
            if resource:
                service_name = resource.attributes.get("service.name") or ""
            else:
                service_name = ""

        record["extra"]["otelServiceName"] = service_name

        span = get_current_span()
        if span != INVALID_SPAN:
            ctx = span.get_span_context()
            if ctx != INVALID_SPAN_CONTEXT:
                record["extra"]["otelSpanID"] = format(ctx.span_id, "016x")
                record["extra"]["otelTraceID"] = format(ctx.trace_id, "032x")
                record["extra"]["otelTraceSampled"] = ctx.trace_flags.sampled

    logger.configure(patcher=add_trace_context)


instrument_loguru()

logger.remove()
format_ = "{time:YYYY-MM-DD HH:MM:SS.sss} {level} [{name}] [{file}:{line} [trace_id={extra[otelTraceID]} span_id={extra[otelSpanID]} resource.service.name={extra[otelServiceName]} trace_sampled={extra[otelTraceSampled]}] - {message}"
logger.add(sys.stderr, format=format_)

logger.info("This is an info message")

This would produce the same output as with the standard logging library.

Delgan avatar Jan 02 '24 18:01 Delgan