clearcut_span_exporter_unittest.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # Copyright 2024 The Chromium Authors
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Unittests for SpanExporter classes."""
  5. import datetime
  6. import re
  7. import time
  8. import urllib.request
  9. from opentelemetry.sdk import trace
  10. from opentelemetry.sdk.trace import export
  11. from .proto import clientanalytics_pb2
  12. from .proto import trace_span_pb2
  13. from . import anonymization
  14. from . import clearcut_span_exporter
  15. class MockResponse:
  16. """Mock requests.Response."""
  17. def __init__(self, status, text) -> None:
  18. self._status = status
  19. self._text = text
  20. def __enter__(self):
  21. return self
  22. def __exit__(self, *args) -> None:
  23. pass
  24. def read(self):
  25. return self._text
  26. tracer = trace.TracerProvider().get_tracer(__name__)
  27. def test_otel_span_translation(monkeypatch) -> None:
  28. """Test ClearcutSpanExporter to translate otel spans to TraceSpan."""
  29. requests = []
  30. def mock_urlopen(request, timeout=0):
  31. requests.append((request, timeout))
  32. resp = clientanalytics_pb2.LogResponse()
  33. resp.next_request_wait_millis = 1
  34. body = resp.SerializeToString()
  35. return MockResponse(200, body)
  36. monkeypatch.setattr(urllib.request, "urlopen", mock_urlopen)
  37. span = tracer.start_span("name")
  38. span.end()
  39. e = clearcut_span_exporter.ClearcutSpanExporter(max_queue_size=1)
  40. assert e.export([span]) == export.SpanExportResult.SUCCESS
  41. req, _ = requests[0]
  42. log_request = clientanalytics_pb2.LogRequest()
  43. log_request.ParseFromString(req.data)
  44. assert log_request.request_time_ms <= int(time.time() * 1000)
  45. assert len(log_request.log_event) == 1
  46. # The following constants are defined in clearcut_span_exporter
  47. # as _CLIENT_TYPE and _LOG_SOURCE respectively.
  48. assert log_request.client_info.client_type == 33
  49. assert log_request.log_source == 2044
  50. tspan = trace_span_pb2.TraceSpan()
  51. tspan.ParseFromString(log_request.log_event[0].source_extension)
  52. assert tspan.name == span.name
  53. assert tspan.start_time_millis == int(span.start_time / 1e6)
  54. assert tspan.end_time_millis == int(span.end_time / 1e6)
  55. def test_otel_span_translation_with_anonymization(monkeypatch) -> None:
  56. """Test ClearcutSpanExporter to anonymize spans to before export."""
  57. requests = []
  58. def mock_urlopen(request, timeout=0):
  59. requests.append((request, timeout))
  60. resp = clientanalytics_pb2.LogResponse()
  61. resp.next_request_wait_millis = 1
  62. body = resp.SerializeToString()
  63. return MockResponse(200, body)
  64. monkeypatch.setattr(urllib.request, "urlopen", mock_urlopen)
  65. span = tracer.start_span("span-user4321")
  66. span.set_attributes({"username": "user4321"})
  67. span.add_event("event-for-user4321")
  68. span.end()
  69. anonymizer = anonymization.Anonymizer([(re.escape("user4321"), "<user>")])
  70. f = anonymization.AnonymizingFilter(anonymizer)
  71. e = clearcut_span_exporter.ClearcutSpanExporter(prefilter=f,
  72. max_queue_size=1)
  73. assert e.export([span]) == export.SpanExportResult.SUCCESS
  74. req, _ = requests[0]
  75. log_request = clientanalytics_pb2.LogRequest()
  76. log_request.ParseFromString(req.data)
  77. tspan = trace_span_pb2.TraceSpan()
  78. tspan.ParseFromString(log_request.log_event[0].source_extension)
  79. assert tspan.name == "span-<user>"
  80. assert tspan.events[0].name == "event-for-<user>"
  81. assert tspan.attributes["username"] == "<user>"
  82. def test_export_to_http_api(monkeypatch) -> None:
  83. """Test ClearcutSpanExporter to export spans over http."""
  84. requests = []
  85. def mock_urlopen(request, timeout=0):
  86. requests.append((request, timeout))
  87. resp = clientanalytics_pb2.LogResponse()
  88. resp.next_request_wait_millis = 1
  89. body = resp.SerializeToString()
  90. return MockResponse(200, body)
  91. monkeypatch.setattr(urllib.request, "urlopen", mock_urlopen)
  92. span = tracer.start_span("name")
  93. span.end()
  94. endpoint = "http://domain.com/path"
  95. e = clearcut_span_exporter.ClearcutSpanExporter(endpoint=endpoint,
  96. timeout=7,
  97. max_queue_size=1)
  98. assert e.export([span])
  99. req, timeout = requests[0]
  100. assert req.full_url == endpoint
  101. assert timeout == 7
  102. def test_export_to_http_api_throttle(monkeypatch) -> None:
  103. """Test ClearcutSpanExporter to throttle based on prev response."""
  104. mock_open_times = []
  105. def mock_urlopen(request, timeout=0):
  106. mock_open_times.append(datetime.datetime.now())
  107. resp = clientanalytics_pb2.LogResponse()
  108. resp.next_request_wait_millis = 1000
  109. body = resp.SerializeToString()
  110. return MockResponse(200, body)
  111. monkeypatch.setattr(urllib.request, "urlopen", mock_urlopen)
  112. span = tracer.start_span("name")
  113. span.end()
  114. e = clearcut_span_exporter.ClearcutSpanExporter(max_queue_size=1)
  115. assert e.export([span])
  116. assert e.export([span])
  117. # We've called export() on the same exporter instance twice, so we expect
  118. # the following things to be true:
  119. # 1. The request.urlopen() function has been called exactly twice, and
  120. # 2. The calls to urlopen() are more than 1000 ms apart (due to the
  121. # value in the mock_urlopen response).
  122. # The mock_open_times list is a proxy for observing this behavior directly.
  123. assert len(mock_open_times) == 2
  124. assert (mock_open_times[1] - mock_open_times[0]).total_seconds() > 1
  125. def test_export_to_drop_spans_if_wait_more_than_threshold(monkeypatch) -> None:
  126. """Test ClearcutSpanExporter to drop span if wait is more than threshold."""
  127. mock_open_times = []
  128. def mock_urlopen(request, timeout=0):
  129. nonlocal mock_open_times
  130. mock_open_times.append(datetime.datetime.now())
  131. resp = clientanalytics_pb2.LogResponse()
  132. resp.next_request_wait_millis = 900000
  133. body = resp.SerializeToString()
  134. return MockResponse(200, body)
  135. monkeypatch.setattr(urllib.request, "urlopen", mock_urlopen)
  136. span = tracer.start_span("name")
  137. span.end()
  138. e = clearcut_span_exporter.ClearcutSpanExporter(max_queue_size=1)
  139. assert e.export([span])
  140. assert e.export([span])
  141. # We've called export() on the same exporter instance twice, so we expect
  142. # the following things to be true:
  143. # 1. The request.urlopen() function has been called exactly once
  144. assert len(mock_open_times) == 1
  145. def test_flush_to_clear_export_queue_to_http_api(monkeypatch) -> None:
  146. """Test ClearcutSpanExporter to export spans on flush."""
  147. requests = []
  148. def mock_urlopen(request, timeout=0):
  149. requests.append((request, timeout))
  150. resp = clientanalytics_pb2.LogResponse()
  151. resp.next_request_wait_millis = 1
  152. body = resp.SerializeToString()
  153. return MockResponse(200, body)
  154. monkeypatch.setattr(urllib.request, "urlopen", mock_urlopen)
  155. span = tracer.start_span("name")
  156. span.end()
  157. e = clearcut_span_exporter.ClearcutSpanExporter(max_queue_size=3)
  158. assert e.export([span])
  159. assert e.export([span])
  160. assert len(requests) == 0
  161. assert e.force_flush()
  162. assert len(requests) == 1
  163. def test_shutdown_to_clear_export_queue_to_http_api(monkeypatch) -> None:
  164. """Test ClearcutSpanExporter to export spans on shutdown."""
  165. requests = []
  166. def mock_urlopen(request, timeout=0):
  167. requests.append((request, timeout))
  168. resp = clientanalytics_pb2.LogResponse()
  169. resp.next_request_wait_millis = 1
  170. body = resp.SerializeToString()
  171. return MockResponse(200, body)
  172. monkeypatch.setattr(urllib.request, "urlopen", mock_urlopen)
  173. span = tracer.start_span("name")
  174. span.end()
  175. e = clearcut_span_exporter.ClearcutSpanExporter(max_queue_size=3)
  176. assert e.export([span])
  177. assert e.export([span])
  178. assert len(requests) == 0
  179. e.shutdown()
  180. assert len(requests) == 1