Skip to content

Commit 06b0b38

Browse files
committed
feat: avoid duplicate logs in cloudwatch
But still maintain the OTel handlers.
1 parent 0dc0500 commit 06b0b38

1 file changed

Lines changed: 53 additions & 5 deletions

File tree

src/corva/logger.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import os
23
import logging
34
import logging.config
45
import sys
@@ -14,10 +15,41 @@
1415

1516
logging.getLogger("urllib3.connectionpool").setLevel(SETTINGS.LOG_LEVEL)
1617

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.
1821
# see https://github.com/corva-ai/otel/pull/37
1922
# 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)]
2153

2254

2355
def get_formatter(
@@ -219,9 +251,25 @@ def set_formatter(self):
219251

220252
def __enter__(self):
221253
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
225273

226274
return self
227275

0 commit comments

Comments
 (0)