fix(otel): recognize inherited logging handlers before assigning formatters

Closed
#581 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@yeongseon is already working on this.

Since Oct 18, 2026.

  • #596 by @yeongseon — open
  • #603 by @yeongseon — merged

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
55/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
azure, python

Research direction

Read src/azure_functions_logging/_setup.py and src/azure_functions_logging/_host_config.py, then inspect the regression in tests/test_setup.py and how it covers the direct SDK handler. Add coverage using an inherited handler and the real SDK in-memory exporter, while preserving support when OTel is absent. Done means both recognition sites agree, the subclass keeps its structured body, and a normal stream handler still receives the formatter.

Written by the indexing model from the issue text.

Description

bug needs-triage priority:medium

Context

At reviewed commit 5343aa68b745e94a40041941f5f35915181ec4ff (0.15.3), Azure-mode setup_logging(functions_formatter=...) recognizes an OpenTelemetry handler only when the concrete class's module starts with opentelemetry.. An ordinary application subclass of the real SDK LoggingHandler therefore loses the protection added in #511.

The subclass still is an OpenTelemetry handler and exports records through the SDK. However, setup installs the Functions formatter on it. A structured mapping log body becomes a JSON string containing another log envelope; the original mapping is reduced to Python repr inside its message. Handler detection also misses the subclass, so no formatter diagnostic is emitted.

Priority is medium because the failure is limited to inherited/customized OTel handlers; using the direct SDK class or configuring the formatter only on the non-OTel output avoids it.

Reproduction

Run in a fresh process with the reviewed package, Python 3.12.14, and opentelemetry-sdk==1.45.1 installed. This uses the real SDK with an in-memory exporter; no Azure resource, exporter endpoint, or credential is involved.

import json
import logging
import os
import warnings

from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import InMemoryLogRecordExporter, SimpleLogRecordProcessor

from azure_functions_logging import JsonFormatter, setup_logging


class ApplicationLoggingHandler(LoggingHandler):
    pass


os.environ['FUNCTIONS_WORKER_RUNTIME'] = 'python'
os.environ['PYTHON_ENABLE_OPENTELEMETRY'] = '1'
exporter = InMemoryLogRecordExporter()
provider = LoggerProvider()
provider.add_log_record_processor(SimpleLogRecordProcessor(exporter))
with warnings.catch_warnings():
    warnings.simplefilter('ignore', DeprecationWarning)
    handler = ApplicationLoggingHandler(logger_provider=provider)
root = logging.getLogger()
root.handlers = [handler]
root.setLevel(logging.INFO)
logger = logging.getLogger('review.otel')
logger.info({'event': 'before', 'count': 1})
with warnings.catch_warnings(record=True) as captured:
    warnings.simplefilter('always')
    setup_logging(functions_formatter=JsonFormatter())
logger.info({'event': 'after', 'count': 1})
print(json.dumps({
    'handler_module': type(handler).__module__,
    'is_otel_handler': isinstance(handler, LoggingHandler),
    'formatter_installed': type(handler.formatter).__name__,
    'warnings': [str(item.message) for item in captured],
    'bodies': [item.log_record.body for item in exporter.get_finished_logs()],
}, indent=2, default=str))
root.handlers = []
provider.shutdown()

Observed:

  • is_otel_handler is true.
  • formatter_installed is JsonFormatter, and warnings is empty.
  • The first exported body is the mapping {"event": "before", "count": 1}.
  • The second body is a string such as {"timestamp": "...", "level": "INFO", "message": "{'event': 'after', 'count': 1}", ...}.

Expected: applying functions_formatter preserves the SDK subclass's native structured body, just as it does for an instance of LoggingHandler itself. Non-OTel handlers should continue receiving the selected formatter.

Cause / fix direction: both formatter selection in _setup.py and handler detection in _host_config.py inspect only type(handler).__module__. Share a capability/ancestry-aware recognition helper, preserving the package's import-free behavior when OTel is absent. An MRO-aware check can recognize supported inherited handlers without adding a mandatory OTel import. Keep detection narrow enough not to treat unrelated logging handlers as OTel handlers.

Acceptance Checklist

  • A real SDK LoggingHandler subclass defined in an application module retains an unformatted mapping body after Azure-mode setup.
  • Direct SDK handlers and supported instrumentation logging handlers retain their current behavior.
  • Formatter selection and configuration diagnostics use the same handler-recognition rule.
  • A coexisting normal stream handler still receives functions_formatter.
  • Add a regression with the actual SDK and in-memory exporter, including the inherited handler case.
  • Importing/configuring the library without the optional OTel package remains supported.

Out of scope

  • Changing OpenTelemetry's serialization/export behavior or creating an exporter.
  • Making arbitrary wrapper classes transparently delegate formatters.
  • Changing root handler ownership or the root logger level in Azure mode.
  • Reopening the direct-SDK-class case fixed by #511; this is the remaining subclass path.

References

Dominant language
Python
Stars
15
Forks
0
Avg merge
7h 53m
Merged PRs (30d)
90

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from yeongseon/azure-functions-logging-python

All issues in yeongseon/azure-functions-logging-python

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.