Skip to content

Commit a5e6609

Browse files
committed
Teach our integrations utils about http tests
And add a test for http otlp exporter user-agent. The http server requires to bind to a random port for each test because as it won't unbind it until process exit. And SO_REUSEPORT does not seem to help either.
1 parent d2d4459 commit a5e6609

File tree

2 files changed

+150
-5
lines changed

2 files changed

+150
-5
lines changed

tests/integration/test_integration.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@
2323
)
2424

2525
from elasticotel.distro import version
26-
from .utils import ElasticIntegrationTestCase, OTEL_INSTRUMENTATION_VERSION, ROOT_DIR
26+
from .utils import (
27+
ElasticIntegrationGRPCTestCase,
28+
ElasticIntegrationHTTPTestCase,
29+
OTEL_INSTRUMENTATION_VERSION,
30+
ROOT_DIR,
31+
)
2732

2833

2934
@pytest.mark.integration
30-
class IntegrationTestCase(ElasticIntegrationTestCase):
35+
class GRPCIntegrationTestCase(ElasticIntegrationGRPCTestCase):
3136
@classmethod
3237
def requirements(cls):
3338
requirements = super().requirements()
@@ -184,7 +189,47 @@ def test_script():
184189

185190

186191
@pytest.mark.integration
187-
class OperatorTestCase(ElasticIntegrationTestCase):
192+
class HTTPIntegrationTestCase(ElasticIntegrationHTTPTestCase):
193+
@classmethod
194+
def requirements(cls):
195+
requirements = super().requirements()
196+
return requirements + [f"opentelemetry-instrumentation-sqlite3=={OTEL_INSTRUMENTATION_VERSION}"]
197+
198+
def test_edot_user_agent_is_used_in_otlp_http_exporter(self):
199+
def test_script():
200+
import sqlite3
201+
202+
from opentelemetry._events import Event, get_event_logger
203+
204+
connection = sqlite3.connect(":memory:")
205+
cursor = connection.cursor()
206+
cursor.execute("CREATE TABLE movie(title, year, score)")
207+
208+
event = Event(name="test.event", attributes={}, body={"key": "value"})
209+
event_logger = get_event_logger(__name__)
210+
event_logger.emit(event)
211+
212+
stdout, stderr, returncode = self.run_script(test_script, wrapper_script="opentelemetry-instrument")
213+
214+
telemetry = self.get_telemetry()
215+
(metrics_headers, logs_headers, traces_headers) = (
216+
telemetry["metrics_headers"],
217+
telemetry["logs_headers"],
218+
telemetry["traces_headers"],
219+
)
220+
221+
assert metrics_headers
222+
assert traces_headers
223+
assert logs_headers
224+
225+
edot_user_agent = "elastic-otlp-http-python/" + version.__version__
226+
self.assertIn(edot_user_agent, metrics_headers[0]["User-Agent"])
227+
self.assertIn(edot_user_agent, traces_headers[0]["User-Agent"])
228+
self.assertIn(edot_user_agent, logs_headers[0]["User-Agent"])
229+
230+
231+
@pytest.mark.integration
232+
class OperatorTestCase(ElasticIntegrationHTTPTestCase):
188233
@staticmethod
189234
def _read_operator_requirements():
190235
requirements = []

tests/integration/utils.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
import base64
1818
import inspect
19+
import random
1920
import subprocess
2021
import os
2122
import tempfile
2223
import unittest
24+
from http.server import BaseHTTPRequestHandler, HTTPServer
2325
from pathlib import Path
2426
from typing import Callable, Mapping, Optional
2527

@@ -33,7 +35,7 @@
3335
ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
3436

3537

36-
class ElasticIntegrationTestCase(unittest.TestCase):
38+
class ElasticIntegrationGRPCTestCase(unittest.TestCase):
3739
"""This is an experimental reimplementation of OtelTest using unittest
3840
3941
The idea is to do integration testing by creating a separate virtualenv for each TestCase inheriting
@@ -42,7 +44,7 @@ class ElasticIntegrationTestCase(unittest.TestCase):
4244
4345
A basic TestCase would look like:
4446
45-
class MyTestCase(ElasticIntegrationTestCase):
47+
class MyTestCase(ElasticIntegrationGRPCTestCase):
4648
@classmethod
4749
def requirements(cls):
4850
requirements = super().requirements()
@@ -274,3 +276,101 @@ def run_script(
274276
ex.stderr.decode() if ex.stderr else None,
275277
proc.returncode,
276278
)
279+
280+
281+
class HttpSink(ot.HttpSink):
282+
"""Backport of teardown fixes, drop if we ever rebase on top of oteltest 0.37.0+"""
283+
284+
# This code is copyright Pablo Collins under Apache-2.0
285+
286+
def __init__(self, listener, port=4318, daemon=True):
287+
self.httpd = None
288+
super().__init__(listener, port, daemon)
289+
290+
def run_server(self):
291+
class Handler(BaseHTTPRequestHandler):
292+
def do_POST(this):
293+
# /v1/traces
294+
content_length = int(this.headers["Content-Length"])
295+
post_data = this.rfile.read(content_length)
296+
297+
otlp_handler_func = self.handlers.get(this.path)
298+
if otlp_handler_func:
299+
otlp_handler_func(post_data, {k: v for k, v in this.headers.items()})
300+
301+
this.send_response(200)
302+
this.send_header("Content-type", "text/html")
303+
this.end_headers()
304+
305+
this.wfile.write("OK".encode("utf-8"))
306+
307+
self.httpd = HTTPServer(("", self.port), Handler)
308+
self.httpd.serve_forever()
309+
310+
def stop(self):
311+
if self.httpd:
312+
self.httpd.shutdown()
313+
super().stop()
314+
315+
316+
class ElasticIntegrationHTTPTestCase(ElasticIntegrationGRPCTestCase):
317+
"""This is an experimental reimplementation of OtelTest using unittest
318+
319+
The idea is to do integration testing by creating a separate virtualenv for each TestCase inheriting
320+
from ElasticIntegrationTestCase, run a script, collect OTLP http/protobuf calls and make the received data
321+
available in order to add assertions.
322+
323+
A basic TestCase would look like:
324+
325+
class MyTestCase(ElasticIntegrationHTTPTestCase):
326+
@classmethod
327+
def requirements(cls):
328+
requirements = super().requirements()
329+
return requirements + [f"my-library"]
330+
331+
def script(self):
332+
import sqlite3
333+
334+
connection = sqlite3.connect(":memory:")
335+
cursor = connection.cursor()
336+
cursor.execute("CREATE TABLE movie(title, year, score)")
337+
338+
def test_one_span_generated(self):
339+
stdout, stderr, returncode = self.run_script(self.script, wrapper_script="opentelemetry-instrument")
340+
341+
telemetry.get_telemetry()
342+
assert len(telemetry["traces"], 1)
343+
344+
345+
Each TestCase costs around 10 seconds for settings up the virtualenv so split tests accordingly.
346+
"""
347+
348+
def set_http_port(self):
349+
self._http_port = int(random.uniform(44318, 45000))
350+
return self._http_port
351+
352+
def get_http_port(self):
353+
return self._http_port
354+
355+
def setUp(self):
356+
self.handler = ot.AccumulatingHandler()
357+
# pick a new port each time because otherwise the next test will fail to bind to the same port, even with SO_REUSEPORT :(
358+
port = self.set_http_port()
359+
self.sink = HttpSink(self.handler, port=port)
360+
self.sink.start()
361+
362+
def run_script(
363+
self,
364+
script_func: Callable[[], None],
365+
environment_variables: Optional[Mapping[str, str]] = None,
366+
wrapper_script: Optional[str] = None,
367+
on_start: Optional[Callable[[], Optional[float]]] = None,
368+
):
369+
# Add a sane default so that callers don't need to remember to set that
370+
if environment_variables is None:
371+
otlp_endpoint = f"http://localhost:{self.get_http_port()}"
372+
environment_variables = {
373+
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
374+
"OTEL_EXPORTER_OTLP_ENDPOINT": otlp_endpoint,
375+
}
376+
return super().run_script(script_func, environment_variables, wrapper_script, on_start)

0 commit comments

Comments
 (0)