|
1 | 1 | import contextlib |
2 | 2 | import logging |
3 | | -import logging.config |
4 | 3 | import sys |
5 | 4 | import time |
| 5 | +from contextlib import suppress |
6 | 6 | from typing import Optional |
7 | 7 |
|
8 | 8 | from corva.configuration import SETTINGS |
|
14 | 14 |
|
15 | 15 | logging.getLogger("urllib3.connectionpool").setLevel(SETTINGS.LOG_LEVEL) |
16 | 16 |
|
17 | | -# unset to pass messages to ancestor loggers, including OTel Log Sending handler |
| 17 | +# Disable propagation to avoid duplicate CloudWatch logs from AWS Lambda's |
| 18 | +# root handler. We will explicitly attach any OTel log handlers directly |
| 19 | +# to CORVA_LOGGER within LoggingContext when log sending is enabled. |
18 | 20 | # see https://github.com/corva-ai/otel/pull/37 |
19 | 21 | # see https://corvaqa.atlassian.net/browse/EE-31 |
20 | | -# CORVA_LOGGER.propagate = False |
| 22 | +CORVA_LOGGER.propagate = False |
| 23 | + |
| 24 | + |
| 25 | +def _is_otel_handler(handler: logging.Handler) -> bool: |
| 26 | + """Best-effort detection of OTel log sending handlers. |
| 27 | +
|
| 28 | + We match by class/module name to avoid importing OTel directly. |
| 29 | + """ |
| 30 | + try: |
| 31 | + module = getattr(handler.__class__, "__module__", "") or "" |
| 32 | + name = handler.__class__.__name__ |
| 33 | + ident = f"{module}.{name}".lower() |
| 34 | + return ("otel" in ident) or ("opentelemetry" in ident) |
| 35 | + except Exception: |
| 36 | + return False |
| 37 | + |
| 38 | + |
| 39 | +def _gather_otel_handlers_from_root() -> list[logging.Handler]: |
| 40 | + """Collect OTel handlers already attached to the root logger. |
| 41 | +
|
| 42 | + We reuse existing handler instances and attach them to CORVA_LOGGER |
| 43 | + to keep OTel log sending while propagation is disabled. |
| 44 | + """ |
| 45 | + root = logging.getLogger() |
| 46 | + handlers = getattr(root, "handlers", []) or [] |
| 47 | + return [h for h in handlers if _is_otel_handler(h)] |
21 | 48 |
|
22 | 49 |
|
23 | 50 | def get_formatter( |
@@ -219,9 +246,24 @@ def set_formatter(self): |
219 | 246 |
|
220 | 247 | def __enter__(self): |
221 | 248 | self.old_handlers = self.logger.handlers |
222 | | - self.logger.handlers = ( |
223 | | - [self.handler, self.user_handler] if self.user_handler else [self.handler] |
224 | | - ) |
| 249 | + |
| 250 | + # Build the handler chain for CORVA_LOGGER. |
| 251 | + handlers = [self.handler] |
| 252 | + if self.user_handler: |
| 253 | + handlers.append(self.user_handler) |
| 254 | + |
| 255 | + # If OTel log sending is enabled and an OTel handler exists on root, |
| 256 | + # attach it to CORVA_LOGGER as well so propagation can remain disabled |
| 257 | + # (avoids AWS root duplication) while still exporting logs via OTel. |
| 258 | + |
| 259 | + if not SETTINGS.OTEL_LOG_SENDING_DISABLED: |
| 260 | + # Fail-safe: never break logging if detection fails |
| 261 | + with suppress(Exception): |
| 262 | + for handler in _gather_otel_handlers_from_root(): |
| 263 | + if handler not in handlers: |
| 264 | + handlers.append(handler) |
| 265 | + |
| 266 | + self.logger.handlers = handlers |
225 | 267 |
|
226 | 268 | return self |
227 | 269 |
|
|
0 commit comments