opentelemetry-python icon indicating copy to clipboard operation
opentelemetry-python copied to clipboard

Logs API: not possible to use without SDK

Open lmolkova opened this issue 1 year ago • 12 comments

Let's assume I'm building a logging bridge for one of many python non-standard loggers (e.g. structlog).

I should be able to create a library using logging (bridge) API alone.

So I should write a code like:

from opentelemetry._logs import get_logger_provider
from opentelemetry._logs import LogRecord as APILogRecord

logger = get_logger_provider().get_logger(__name__)

logger.emit(APILogRecord(body="Hello, World!", attributes={"foo": "bar"}))

It'd fail with

opentelemetry\sdk\_logs\_internal\export\__init__.py", line 97, in <lambda>
    formatter: Callable[[LogRecord], str] = lambda record: record.to_json()
                                                           ^^^^^^^^^^^^^^
AttributeError: 'LogRecord' object has no attribute 'to_json'

Probably related to #3552


The recommendation

  1. Make it possible to (properly) use Logs API alone without touching SDK types
  2. Add example and tests that use vanilla logs API ~~(with note that it's for logging bridges, not end users)~~ (update - this is not necessary anymore, logs API is now for everyone to use)
  3. Call API and SDK log records differently to minimize confusion. E.g. SDK one could be ReadWriteLogRecord and should probably replace LogData (see #4313)
  4. Consider using a pattern similar to tracing when users don't need to instantiate a new span. Instead they could call logger.emit(....) with parameters and the SDK implementation of logger will create SDK implementation of LogRecord

Part of https://github.com/open-telemetry/community/issues/1751

lmolkova avatar Nov 24 '24 19:11 lmolkova

This is part of the same issue - if I try to use SDK LogRecord directly like in this snippet

from opentelemetry._logs import LogRecord as APILogRecord
from opentelemetry._logs import get_logger_provider
from opentelemetry.sdk._logs import LogRecord as SDKLogRecord

...
resource = Resource.create({"service.name": "test logs"})
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(SimpleLogRecordProcessor(ConsoleLogExporter()))
set_logger_provider(logger_provider)

logger = get_logger_provider().get_logger(__name__)

with trace_provider.get_tracer("example").start_as_current_span("foo"):
    logger.emit(SDKLogRecord(body="Hello, World!", attributes={"foo": "bar"}))

I get into the following issues:

  1. I have to provide timestamp even though it's optional

        formatter: Callable[[LogRecord], str] = lambda record: record.to_json()
                                                               ^^^^^^^^^^^^^^^^
      File "opentelemetry\sdk\_logs\_internal\__init__.py", line 230, in to_json
        "timestamp": ns_to_iso_str(self.timestamp),
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "opentelemetry\sdk\util\__init__.py", line 27, in ns_to_iso_str
        nanoseconds / 1e9, tz=datetime.timezone.utc
        ~~~~~~~~~~~~^~~~~
    TypeError: unsupported operand type(s) for /: 'NoneType' and 'float'
    
  2. If I provide it (logger.emit(SDKLogRecord(timestamp=time_ns(), body="Hello, World!", attributes={"foo": "bar"}))) I don't get resource attributes set

    {
        "body": "Hello, World!",
        "severity_number": "None",
        "severity_text": null,
        "attributes": {
            "foo": "bar"
        },
        "dropped_attributes": 0,
        "timestamp": "2024-11-24T19:48:29.322928Z",
        "observed_timestamp": "2024-11-24T19:48:29.322928Z",
        "trace_id": "",
        "span_id": "",
        "trace_flags": null,
        "resource": {
            "attributes": {
                "telemetry.sdk.language": "python",
                "telemetry.sdk.name": "opentelemetry",
                "telemetry.sdk.version": "1.28.2",
                "service.name": "unknown_service"
            },
            "schema_url": ""
        }
    }
    
  3. Also you can see that trace-context is not set despite having valid current span

lmolkova avatar Nov 24 '24 19:11 lmolkova

Probably related to https://github.com/open-telemetry/opentelemetry-python/issues/3552

When using the APILogRecord I get:

  • https://github.com/open-telemetry/opentelemetry-python/issues/4319 on ConsoleLogExporter
  • https://github.com/open-telemetry/opentelemetry-python/issues/3552 on OTLPLogExporter

luccabb avatar Feb 12 '25 02:02 luccabb

probably also part of the same issue, resource and scope attributes via the SDK are ignored:

from opentelemetry._logs import LogRecord, get_logger_provider, set_logger_provider, SeverityNumber
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LogRecord, LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.trace.span import TraceFlags

resource = Resource(attributes={"resource_attr": "value"})
logger_provider = LoggerProvider(resource=resource)
exporter = OTLPLogExporter(endpoint="http://localhost:4318/v1/logs", timeout=10)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
set_logger_provider(logger_provider)
logger = logger_provider.get_logger("test_logger", attributes={"scope_attr": "value"})
# also fails with:
# logger = get_logger_provider().get_logger("test_logger", attributes={"scope_attr": "value"})

logger.emit(
	LogRecord(
                body="Hello, World!", 
                attributes={"datapoint_attr": "value"},
                span_id=0, 
                trace_id=0, 
                trace_flags=TraceFlags.get_default(),
                severity_number=SeverityNumber.UNSPECIFIED,
	)
)

produces:

{
  "resourceLogs": [
    {
      "resource": {
        "attributes": [
          {
            "key": "telemetry.sdk.language",
            "value": {
              "stringValue": "python"
            }
          },
          {
            "key": "telemetry.sdk.name",
            "value": {
              "stringValue": "opentelemetry"
            }
          },
          {
            "key": "telemetry.sdk.version",
            "value": {
              "stringValue": "1.23.0"
            }
          },
          {
            "key": "service.name",
            "value": {
              "stringValue": "unknown_service"
            }
          }
        ]
      },
      "scopeLogs": [
        {
          "scope": {
            "name": "test_logger"
          },
          "logRecords": [
            {
              "observedTimeUnixNano": "1739490609316028637",
              "body": {
                "stringValue": "Hello, World!"
              },
              "attributes": [
                {
                  "key": "datapoint_attr",
                  "value": {
                    "stringValue": "value"
                  }
                }
              ],
              "traceId": "",
              "spanId": ""
            }
          ]
        }
      ]
    }
  ]
}

luccabb avatar Feb 14 '25 00:02 luccabb

I worked around both of these issues. Pass the resource in the LogRecord like this:

LogRecord(
      body="Hello, World!", 
      attributes={"datapoint_attr": "value"},
      span_id=0, 
      trace_id=0, 
      trace_flags=TraceFlags.get_default(),
      severity_number=SeverityNumber.UNSPECIFIED,
      resource=logger.resource,
)

The implementation is probably not ideal, but both of the examples above got me to a working example.

The LoggingHandler code in the SDK does it like that.

jhodnett2 avatar Mar 22 '25 01:03 jhodnett2

Yes we definitely need to sort this out and there will probably be some breaking changes before marking the logging API stable

aabmass avatar May 29 '25 16:05 aabmass

I believe I had a prototype for this one - https://github.com/open-telemetry/opentelemetry-python/pull/4213, which is probably quite out of date, but demonstrates the proposal

lmolkova avatar May 29 '25 16:05 lmolkova

I guess it is not worth building anything with the Logs API yet? Is there any reasonable workaround to this issue?

This is what I came up with but this is probably going to break at any time 🤔

span_context = span.get_span_context()

record = LogRecord(
    body='Using only the Logs API.',
    trace_id=span_context.trace_id,
    span_id=span_context.span_id,
    trace_flags=span_context.trace_flags,
    severity_number=SeverityNumber.WARN,
    severity_text="WARN",
    attributes={'foo': 'bar'},
)
# Attributes that only exist on SDK's flavour of LogRecord, and are apparently assumed to exist
record.resource = logger.resource
record.dropped_attributes = 0

logger.emit(record)

vytas7 avatar Jun 07 '25 13:06 vytas7

Mildly related, the SDK LogRecord has this:

        self.resource = (
            resource if isinstance(resource, Resource) else Resource.create({})
        )

Resource.create({}) is unreasonably slow because it creates a thread pool. Demo:

import time

from opentelemetry.sdk._logs import LogRecord
from opentelemetry.sdk.resources import Resource

for resource in [None, Resource.create({})]:
    start = time.time()
    for _ in range(1000):
        LogRecord(resource=resource)
    end = time.time()
    print(f'Resource: {resource}')
    print(f'Time taken: {end - start:.6f} seconds')

Output:

Resource: None
Time taken: 4.610783 seconds
Resource: <opentelemetry.sdk.resources.Resource object at 0x115841ac0>
Time taken: 0.001270 seconds

alexmojaki avatar Jul 11 '25 19:07 alexmojaki

Consider using a pattern similar to tracing when users don't need to instantiate a new span. Instead they could call logger.emit(....) with parameters and the SDK implementation of logger will create SDK implementation of LogRecord

Agreed, the spec seems to imply that this is the way in https://opentelemetry.io/docs/specs/otel/logs/api/#emit-a-logrecord:

Image Image

I don't think LogRecord is meant to be constructed at all.

alexmojaki avatar Jul 11 '25 19:07 alexmojaki

I ran into this as well - came up with an ugly hack in case anyone needs it temporarily

https://github.com/elastic/elastic-otel-python-instrumentations/pull/91/files#diff-12057a9886c2cf936854ee654cbb7a0d400d22490fc2cd3607a331b1377e6284R352

I skimmed through this issue and the attached PR but am a bit confused by the direction. The interfaces generally seem fine to me, and notably I was surprised to see LogData removed which is a well defined concept in OTel. Isn't the issue just that emit should take keyword arguments instead of a record itself? Both methods just become something like

def emit(self, _deprecated_record: LogRecord | None = None, timestamp=, severity_text=, etc):
  # Possily keep this and `deprecated_record` above around temporarily for compatibility
  # Or maybe it's too much in which case just drop them
  if isinstance(_deprecated_record, LogRecord):
    record = deprecated_record
  else:
    # If we're an API logger, this `LogRecord` is the API one, if we're an SDK logger, it's the SDK one
    # since they are in the same file.
    record = LogRecord(
      timestamp=timestamp,
      severity_text=severity_text,
      etc
    )

It seems easy to use, Pythonic, similar to how tracer works, and a very small change in logic / data structures. Maybe I'm missing something but a share just in case.

anuraaga avatar Sep 04 '25 08:09 anuraaga

I've opened https://github.com/open-telemetry/opentelemetry-python/pull/4737/ to followup @anuraaga suggestion and fix the serialization when using the plain api logrecord. What do you think? I think this may be a good intermediate step. The 2 serialization fixes are not dependent to the emit interface change.

xrmx avatar Sep 08 '25 16:09 xrmx

I've opened #4737 to followup @anuraaga suggestion and fix the serialization when using the plain api logrecord. What do you think? I think this may be a good intermediate step. The 2 serialization fixes are not dependent to the emit interface change.

Alternatively I've also opened with just the fixes and no interface changes that may be easier to get into the next release https://github.com/open-telemetry/opentelemetry-python/pull/4741

xrmx avatar Sep 09 '25 09:09 xrmx