|
14 | 14 |
|
15 | 15 | import json
|
16 | 16 | import logging
|
| 17 | +import sys |
| 18 | +from traceback import format_tb |
17 | 19 |
|
18 | 20 | import pytest
|
19 | 21 |
|
@@ -47,6 +49,22 @@ def log_buffer(caplog):
|
47 | 49 | _logger.removeHandler(_handler)
|
48 | 50 |
|
49 | 51 |
|
| 52 | +@pytest.fixture |
| 53 | +def log_buffer_with_stack_trace(caplog): |
| 54 | + buf = Buffer() |
| 55 | + |
| 56 | + _formatter = NewRelicContextFormatter("", datefmt="ISO8601", stack_trace_limit=None) |
| 57 | + _handler = logging.StreamHandler(buf) |
| 58 | + _handler.setFormatter(_formatter) |
| 59 | + |
| 60 | + _logger.addHandler(_handler) |
| 61 | + caplog.set_level(logging.INFO, logger=__name__) |
| 62 | + |
| 63 | + yield buf |
| 64 | + |
| 65 | + _logger.removeHandler(_handler) |
| 66 | + |
| 67 | + |
50 | 68 | class NonPrintableObject(object):
|
51 | 69 | def __str__(self):
|
52 | 70 | raise RuntimeError("Unable to print object.")
|
@@ -174,7 +192,7 @@ class ExceptionForTest(ValueError):
|
174 | 192 |
|
175 | 193 |
|
176 | 194 | @background_task()
|
177 |
| -def test_newrelic_logger_error_inside_transaction(log_buffer): |
| 195 | +def test_newrelic_logger_error_inside_transaction_no_stack_trace(log_buffer): |
178 | 196 | try:
|
179 | 197 | raise ExceptionForTest
|
180 | 198 | except ExceptionForTest:
|
@@ -220,7 +238,59 @@ def test_newrelic_logger_error_inside_transaction(log_buffer):
|
220 | 238 | assert set(message.keys()) == set(expected_extra_txn_keys)
|
221 | 239 |
|
222 | 240 |
|
223 |
| -def test_newrelic_logger_error_outside_transaction(log_buffer): |
| 241 | +@background_task() |
| 242 | +def test_newrelic_logger_error_inside_transaction_with_stack_trace(log_buffer_with_stack_trace): |
| 243 | + expected_stack_trace = "" |
| 244 | + try: |
| 245 | + raise ExceptionForTest |
| 246 | + except ExceptionForTest: |
| 247 | + _logger.exception("oops") |
| 248 | + expected_stack_trace = "".join(format_tb(sys.exc_info()[2])) |
| 249 | + |
| 250 | + log_buffer_with_stack_trace.seek(0) |
| 251 | + message = json.load(log_buffer_with_stack_trace) |
| 252 | + |
| 253 | + timestamp = message.pop("timestamp") |
| 254 | + thread_id = message.pop("thread.id") |
| 255 | + process_id = message.pop("process.id") |
| 256 | + filename = message.pop("file.name") |
| 257 | + line_number = message.pop("line.number") |
| 258 | + stack_trace = message.pop("error.stack_trace") |
| 259 | + |
| 260 | + assert isinstance(timestamp, int) |
| 261 | + assert isinstance(thread_id, int) |
| 262 | + assert isinstance(process_id, int) |
| 263 | + assert filename.endswith("/test_logs_in_context.py") |
| 264 | + assert isinstance(line_number, int) |
| 265 | + assert isinstance(stack_trace, six.string_types) |
| 266 | + assert stack_trace and stack_trace == expected_stack_trace |
| 267 | + |
| 268 | + expected = { |
| 269 | + "entity.name": "Python Agent Test (agent_features)", |
| 270 | + "entity.type": "SERVICE", |
| 271 | + "message": "oops", |
| 272 | + "log.level": "ERROR", |
| 273 | + "logger.name": "test_logs_in_context", |
| 274 | + "thread.name": "MainThread", |
| 275 | + "process.name": "MainProcess", |
| 276 | + "error.class": "test_logs_in_context:ExceptionForTest", |
| 277 | + "error.message": "", |
| 278 | + "error.expected": False |
| 279 | + } |
| 280 | + expected_extra_txn_keys = ( |
| 281 | + "trace.id", |
| 282 | + "span.id", |
| 283 | + "entity.guid", |
| 284 | + "hostname" |
| 285 | + ) |
| 286 | + |
| 287 | + for k, v in expected.items(): |
| 288 | + assert message.pop(k) == v |
| 289 | + |
| 290 | + assert set(message.keys()) == set(expected_extra_txn_keys) |
| 291 | + |
| 292 | + |
| 293 | +def test_newrelic_logger_error_outside_transaction_no_stack_trace(log_buffer): |
224 | 294 | try:
|
225 | 295 | raise ExceptionForTest
|
226 | 296 | except ExceptionForTest:
|
@@ -263,6 +333,54 @@ def test_newrelic_logger_error_outside_transaction(log_buffer):
|
263 | 333 | assert set(message.keys()) == set(expected_extra_txn_keys)
|
264 | 334 |
|
265 | 335 |
|
| 336 | +def test_newrelic_logger_error_outside_transaction_with_stack_trace(log_buffer_with_stack_trace): |
| 337 | + expected_stack_trace = "" |
| 338 | + try: |
| 339 | + raise ExceptionForTest |
| 340 | + except ExceptionForTest: |
| 341 | + _logger.exception("oops") |
| 342 | + expected_stack_trace = "".join(format_tb(sys.exc_info()[2])) |
| 343 | + |
| 344 | + log_buffer_with_stack_trace.seek(0) |
| 345 | + message = json.load(log_buffer_with_stack_trace) |
| 346 | + |
| 347 | + timestamp = message.pop("timestamp") |
| 348 | + thread_id = message.pop("thread.id") |
| 349 | + process_id = message.pop("process.id") |
| 350 | + filename = message.pop("file.name") |
| 351 | + line_number = message.pop("line.number") |
| 352 | + stack_trace = message.pop("error.stack_trace") |
| 353 | + |
| 354 | + assert isinstance(timestamp, int) |
| 355 | + assert isinstance(thread_id, int) |
| 356 | + assert isinstance(process_id, int) |
| 357 | + assert filename.endswith("/test_logs_in_context.py") |
| 358 | + assert isinstance(line_number, int) |
| 359 | + assert isinstance(stack_trace, six.string_types) |
| 360 | + assert stack_trace and stack_trace == expected_stack_trace |
| 361 | + |
| 362 | + expected = { |
| 363 | + "entity.name": "Python Agent Test (agent_features)", |
| 364 | + "entity.type": "SERVICE", |
| 365 | + "message": "oops", |
| 366 | + "log.level": "ERROR", |
| 367 | + "logger.name": "test_logs_in_context", |
| 368 | + "thread.name": "MainThread", |
| 369 | + "process.name": "MainProcess", |
| 370 | + "error.class": "test_logs_in_context:ExceptionForTest", |
| 371 | + "error.message": "", |
| 372 | + } |
| 373 | + expected_extra_txn_keys = ( |
| 374 | + "entity.guid", |
| 375 | + "hostname", |
| 376 | + ) |
| 377 | + |
| 378 | + for k, v in expected.items(): |
| 379 | + assert message.pop(k) == v |
| 380 | + |
| 381 | + assert set(message.keys()) == set(expected_extra_txn_keys) |
| 382 | + |
| 383 | + |
266 | 384 | EXPECTED_KEYS_TXN = (
|
267 | 385 | "trace.id",
|
268 | 386 | "span.id",
|
|
0 commit comments