Skip to content

Commit 9999366

Browse files
authored
Django configuration compatibility (#24)
* use lazy initialization with a class lock * add urls and scout support as authors * mock test-ingest-key in fixture * set handling log after check fix existing tests * add test of new initialize logic * make otel_handler a class variable
1 parent e254efc commit 9999366

File tree

3 files changed

+132
-95
lines changed

3 files changed

+132
-95
lines changed

pyproject.toml

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
[tool.poetry]
22
name = "scout-apm-logging"
3-
version = "1.0.0"
3+
version = "1.0.1"
44
description = "Scout APM Python Logging Agent"
5-
authors = ["Quinn Milionis <[email protected]>"]
5+
authors = ["Scout <[email protected]>"]
6+
maintainers = ["Quinn Milionis <[email protected]>"]
67
readme = "README.md"
78

9+
[tool.poetry.urls]
10+
homepage = "https://scoutapm.com"
11+
repository = "https://github.com/scoutapp/scout_apm_python_logging"
12+
documentation = "https://scoutapm.com/docs/features/log-management"
13+
814
[tool.poetry.dependencies]
915
python = "^3.9"
1016
opentelemetry-api = "^1.26.0"

scout_apm_logging/handler.py

+63-50
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,28 @@
1212

1313

1414
class ScoutOtelHandler(logging.Handler):
15+
_initialization_lock = threading.Lock()
16+
otel_handler = None
17+
1518
def __init__(self, service_name):
1619
super().__init__()
1720
self.logger_provider = None
18-
self.service_name = self._get_service_name(service_name)
19-
self.ingest_key = self._get_ingest_key()
20-
self.endpoint = self._get_endpoint()
21-
self.setup_logging()
21+
self.service_name = service_name
2222
self._handling_log = threading.local()
23+
self._initialized = False
24+
self._initializing = False
25+
26+
def _initialize(self):
27+
with self._initialization_lock:
28+
if ScoutOtelHandler.otel_handler:
29+
return
2330

24-
def setup_logging(self):
31+
self.service_name = self._get_service_name(self.service_name)
32+
self.ingest_key = self._get_ingest_key()
33+
self.endpoint = self._get_endpoint()
34+
self.setup_otel_handler()
35+
36+
def setup_otel_handler(self):
2537
self.logger_provider = LoggerProvider(
2638
resource=Resource.create(
2739
{
@@ -40,56 +52,57 @@ def setup_logging(self):
4052
BatchLogRecordProcessor(otlp_exporter)
4153
)
4254

43-
self.otel_handler = LoggingHandler(
55+
ScoutOtelHandler.otel_handler = LoggingHandler(
4456
level=logging.NOTSET, logger_provider=self.logger_provider
4557
)
4658

4759
def emit(self, record):
60+
if not ScoutOtelHandler.otel_handler:
61+
try:
62+
self._initialize()
63+
except Exception as e:
64+
print(f"Failed to initialize ScoutOtelHandler: {e}")
65+
return
66+
4867
if getattr(self._handling_log, "value", False):
49-
# We're already handling a log message, don't try to get the TrackedRequest
50-
return self.otel_handler.emit(record)
51-
52-
try:
53-
self._handling_log.value = True
54-
scout_request = TrackedRequest.instance()
55-
56-
if scout_request:
57-
operation_detail = get_operation_detail(scout_request)
58-
if operation_detail:
59-
setattr(
60-
record,
61-
operation_detail.entrypoint_attribute,
62-
operation_detail.name,
63-
)
64-
65-
# Add Scout-specific attributes to the log record
66-
record.scout_request_id = scout_request.request_id
67-
record.scout_start_time = scout_request.start_time.isoformat()
68-
if scout_request.end_time:
69-
record.scout_end_time = scout_request.end_time.isoformat()
70-
71-
# Add duration if the request is completed
72-
if scout_request.end_time:
73-
record.scout_duration = (
74-
scout_request.end_time - scout_request.start_time
75-
).total_seconds()
76-
77-
setattr(record, "service.name", self.service_name)
78-
79-
# Add tags
80-
for key, value in scout_request.tags.items():
81-
setattr(record, f"scout_tag_{key}", value)
82-
83-
# Add the current span's operation if available
84-
current_span = scout_request.current_span()
85-
if current_span:
86-
record.scout_current_operation = current_span.operation
87-
88-
self.otel_handler.emit(record)
89-
except Exception as e:
90-
print(f"Error in ScoutOtelHandler.emit: {e}")
91-
finally:
92-
self._handling_log.value = False
68+
# We're already handling a log message, don't get the TrackedRequest
69+
return ScoutOtelHandler.otel_handler.emit(record)
70+
71+
self._handling_log.value = True
72+
scout_request = TrackedRequest.instance()
73+
74+
if scout_request:
75+
operation_detail = get_operation_detail(scout_request)
76+
if operation_detail:
77+
setattr(
78+
record,
79+
operation_detail.entrypoint_attribute,
80+
operation_detail.name,
81+
)
82+
83+
# Add Scout-specific attributes to the log record
84+
record.scout_request_id = scout_request.request_id
85+
record.scout_start_time = scout_request.start_time.isoformat()
86+
# Add duration if the request is completed
87+
if scout_request.end_time:
88+
record.scout_end_time = scout_request.end_time.isoformat()
89+
record.scout_duration = (
90+
scout_request.end_time - scout_request.start_time
91+
).total_seconds()
92+
93+
setattr(record, "service.name", self.service_name)
94+
95+
# Add tags
96+
for key, value in scout_request.tags.items():
97+
setattr(record, f"scout_tag_{key}", value)
98+
99+
# Add the current span's operation if available
100+
current_span = scout_request.current_span()
101+
if current_span:
102+
record.scout_current_operation = current_span.operation
103+
104+
ScoutOtelHandler.otel_handler.emit(record)
105+
self._handling_log.value = False
93106

94107
def close(self):
95108
if self.logger_provider:

tests/unit/test_handler.py

+61-43
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@
88

99
@pytest.fixture
1010
def otel_scout_handler():
11-
with patch("scout_apm_logging.handler.OTLPLogExporter"), patch(
12-
"scout_apm_logging.handler.LoggerProvider"
13-
), patch("scout_apm_logging.handler.BatchLogRecordProcessor"), patch(
11+
# Reset class initialization state
12+
ScoutOtelHandler.otel_handler = None
13+
14+
with patch("scout_apm_logging.handler.scout_config") as mock_scout_config, patch(
15+
"scout_apm_logging.handler.OTLPLogExporter"
16+
), patch("scout_apm_logging.handler.LoggerProvider"), patch(
17+
"scout_apm_logging.handler.BatchLogRecordProcessor"
18+
), patch(
1419
"scout_apm_logging.handler.Resource"
1520
):
21+
22+
mock_scout_config.value.return_value = "test-ingest-key"
1623
handler = ScoutOtelHandler(service_name="test-service")
17-
yield handler
24+
# Force initialization
25+
handler._initialize()
26+
return handler
1827

1928

2029
def test_init(otel_scout_handler):
@@ -37,7 +46,7 @@ def test_emit_with_scout_request(mock_tracked_request, otel_scout_handler):
3746
]
3847
mock_tracked_request.instance.return_value = mock_request
3948

40-
with patch.object(otel_scout_handler, "otel_handler") as mock_otel_handler:
49+
with patch.object(ScoutOtelHandler, "otel_handler") as mock_otel_handler:
4150
record = logging.LogRecord(
4251
name="test",
4352
level=logging.INFO,
@@ -72,7 +81,7 @@ def test_emit_when_scout_request_contains_operation(
7281
mock_request.operation = MagicMock().return_value = "Controller/foobar"
7382
mock_tracked_request.instance.return_value = mock_request
7483

75-
with patch.object(otel_scout_handler, "otel_handler") as mock_otel_handler:
84+
with patch.object(ScoutOtelHandler, "otel_handler") as mock_otel_handler:
7685
record = logging.LogRecord(
7786
name="test",
7887
level=logging.INFO,
@@ -84,18 +93,18 @@ def test_emit_when_scout_request_contains_operation(
8493
)
8594
otel_scout_handler.emit(record)
8695

87-
mock_otel_handler.emit.assert_called_once()
88-
assert record.scout_request_id == "test-id"
89-
assert record.scout_start_time == "2024-03-06T12:00:00"
90-
assert record.scout_end_time == "2024-03-06T12:00:01"
91-
assert record.scout_tag_key == "value"
92-
assert record.controller_entrypoint == "foobar"
96+
mock_otel_handler.emit.assert_called_once()
97+
assert record.scout_request_id == "test-id"
98+
assert record.scout_start_time == "2024-03-06T12:00:00"
99+
assert record.scout_end_time == "2024-03-06T12:00:01"
100+
assert record.scout_tag_key == "value"
101+
assert record.controller_entrypoint == "foobar"
93102

94103

95104
@patch("scout_apm_logging.handler.TrackedRequest")
96105
def test_emit_without_scout_request(mock_tracked_request, otel_scout_handler):
97106
mock_tracked_request.instance.return_value = None
98-
with patch.object(otel_scout_handler, "otel_handler") as mock_otel_handler:
107+
with patch.object(ScoutOtelHandler, "otel_handler") as mock_otel_handler:
99108
record = logging.LogRecord(
100109
name="test",
101110
level=logging.INFO,
@@ -112,7 +121,7 @@ def test_emit_without_scout_request(mock_tracked_request, otel_scout_handler):
112121

113122

114123
def test_emit_already_handling_log(otel_scout_handler):
115-
with patch.object(otel_scout_handler, "otel_handler") as mock_otel_handler:
124+
with patch.object(ScoutOtelHandler, "otel_handler") as mock_otel_handler:
116125
# Simulate that we're already handling a log message
117126
otel_scout_handler._handling_log.value = True
118127

@@ -132,35 +141,6 @@ def test_emit_already_handling_log(otel_scout_handler):
132141
otel_scout_handler._handling_log.value = False
133142

134143

135-
def test_emit_exception_handling(otel_scout_handler):
136-
with patch(
137-
"scout_apm_logging.handler.TrackedRequest"
138-
) as mock_tracked_request, patch(
139-
"sys.stdout", new_callable=io.StringIO
140-
) as mock_stdout:
141-
142-
mock_tracked_request.instance.side_effect = Exception("Test exception")
143-
144-
record = logging.LogRecord(
145-
name="test",
146-
level=logging.INFO,
147-
pathname="",
148-
lineno=0,
149-
msg="Test message",
150-
args=(),
151-
exc_info=None,
152-
)
153-
154-
otel_scout_handler.emit(record)
155-
156-
# Check that the exception was caught and the error message was printed
157-
assert (
158-
"Error in ScoutOtelHandler.emit: Test exception" in mock_stdout.getvalue()
159-
)
160-
161-
otel_scout_handler._handling_log.value = False
162-
163-
164144
def test_close(otel_scout_handler):
165145
with patch.object(otel_scout_handler.logger_provider, "shutdown") as mock_shutdown:
166146
otel_scout_handler.close()
@@ -195,3 +175,41 @@ def test_get_ingest_key_not_set(mock_scout_config, otel_scout_handler):
195175
mock_scout_config.value.return_value = None
196176
with pytest.raises(ValueError, match="SCOUT_LOGS_INGEST_KEY is not set"):
197177
otel_scout_handler._get_ingest_key()
178+
179+
180+
def test_initialize_only_once(otel_scout_handler):
181+
# First initialization happens in fixture
182+
initial_service_name = otel_scout_handler.service_name
183+
184+
# Try to initialize again
185+
otel_scout_handler._initialize()
186+
187+
# Service name should not change since second initialization should return early
188+
assert otel_scout_handler.service_name == initial_service_name
189+
190+
191+
def test_emit_handles_initialization_failure():
192+
with patch("scout_apm_logging.handler.scout_config") as mock_scout_config:
193+
mock_scout_config.value.return_value = (
194+
None # This will cause _get_ingest_key to fail
195+
)
196+
ScoutOtelHandler.otel_handler = None
197+
198+
handler = ScoutOtelHandler(service_name="test-service")
199+
200+
with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
201+
record = logging.LogRecord(
202+
name="test",
203+
level=logging.INFO,
204+
pathname="",
205+
lineno=0,
206+
msg="Test message",
207+
args=(),
208+
exc_info=None,
209+
)
210+
handler.emit(record)
211+
212+
assert (
213+
"Failed to initialize ScoutOtelHandler: "
214+
"SCOUT_LOGS_INGEST_KEY is not set" in mock_stdout.getvalue()
215+
)

0 commit comments

Comments
 (0)