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