import MySQLdb from ddtrace import Pin from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY from ddtrace.contrib.mysqldb.patch import patch, unpatch from tests.opentracer.utils import init_tracer from ..config import MYSQL_CONFIG from ...base import BaseTracerTestCase from ...util import assert_dict_issuperset class MySQLCore(object): """Base test case for MySQL drivers""" conn = None TEST_SERVICE = 'test-mysql' def setUp(self): super(MySQLCore, self).setUp() patch() def tearDown(self): super(MySQLCore, self).tearDown() # Reuse the connection across tests if self.conn: try: self.conn.ping() except MySQLdb.InterfaceError: pass else: self.conn.close() unpatch() def _get_conn_tracer(self): # implement me pass def test_simple_query(self): conn, tracer = self._get_conn_tracer() writer = tracer.writer cursor = conn.cursor() rowcount = cursor.execute('SELECT 1') assert rowcount == 1 rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() assert len(spans) == 1 span = spans[0] assert span.service == self.TEST_SERVICE assert span.name == 'mysql.query' assert span.span_type == 'sql' assert span.error == 0 assert span.get_metric('out.port') == 3306 assert_dict_issuperset(span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) def test_simple_query_fetchall(self): with self.override_config('dbapi2', dict(trace_fetch_methods=True)): conn, tracer = self._get_conn_tracer() writer = tracer.writer cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() assert len(spans) == 2 span = spans[0] assert span.service == self.TEST_SERVICE assert span.name == 'mysql.query' assert span.span_type == 'sql' assert span.error == 0 assert span.get_metric('out.port') == 3306 assert_dict_issuperset(span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) fetch_span = spans[1] assert fetch_span.name == 'mysql.query.fetchall' def test_simple_query_with_positional_args(self): conn, tracer = self._get_conn_tracer_with_positional_args() writer = tracer.writer cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() assert len(spans) == 1 span = spans[0] assert span.service == self.TEST_SERVICE assert span.name == 'mysql.query' assert span.span_type == 'sql' assert span.error == 0 assert span.get_metric('out.port') == 3306 assert_dict_issuperset(span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) def test_simple_query_with_positional_args_fetchall(self): with self.override_config('dbapi2', dict(trace_fetch_methods=True)): conn, tracer = self._get_conn_tracer_with_positional_args() writer = tracer.writer cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() assert len(spans) == 2 span = spans[0] assert span.service == self.TEST_SERVICE assert span.name == 'mysql.query' assert span.span_type == 'sql' assert span.error == 0 assert span.get_metric('out.port') == 3306 assert_dict_issuperset(span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) fetch_span = spans[1] assert fetch_span.name == 'mysql.query.fetchall' def test_query_with_several_rows(self): conn, tracer = self._get_conn_tracer() writer = tracer.writer cursor = conn.cursor() query = 'SELECT n FROM (SELECT 42 n UNION SELECT 421 UNION SELECT 4210) m' cursor.execute(query) rows = cursor.fetchall() assert len(rows) == 3 spans = writer.pop() assert len(spans) == 1 span = spans[0] assert span.get_tag('sql.query') is None def test_query_with_several_rows_fetchall(self): with self.override_config('dbapi2', dict(trace_fetch_methods=True)): conn, tracer = self._get_conn_tracer() writer = tracer.writer cursor = conn.cursor() query = 'SELECT n FROM (SELECT 42 n UNION SELECT 421 UNION SELECT 4210) m' cursor.execute(query) rows = cursor.fetchall() assert len(rows) == 3 spans = writer.pop() assert len(spans) == 2 span = spans[0] assert span.get_tag('sql.query') is None fetch_span = spans[1] assert fetch_span.name == 'mysql.query.fetchall' def test_query_many(self): # tests that the executemany method is correctly wrapped. conn, tracer = self._get_conn_tracer() writer = tracer.writer tracer.enabled = False cursor = conn.cursor() cursor.execute(""" create table if not exists dummy ( dummy_key VARCHAR(32) PRIMARY KEY, dummy_value TEXT NOT NULL)""") tracer.enabled = True stmt = 'INSERT INTO dummy (dummy_key, dummy_value) VALUES (%s, %s)' data = [ ('foo', 'this is foo'), ('bar', 'this is bar'), ] cursor.executemany(stmt, data) query = 'SELECT dummy_key, dummy_value FROM dummy ORDER BY dummy_key' cursor.execute(query) rows = cursor.fetchall() assert len(rows) == 2 assert rows[0][0] == 'bar' assert rows[0][1] == 'this is bar' assert rows[1][0] == 'foo' assert rows[1][1] == 'this is foo' spans = writer.pop() assert len(spans) == 2 span = spans[1] assert span.get_tag('sql.query') is None cursor.execute('drop table if exists dummy') def test_query_many_fetchall(self): with self.override_config('dbapi2', dict(trace_fetch_methods=True)): # tests that the executemany method is correctly wrapped. conn, tracer = self._get_conn_tracer() writer = tracer.writer tracer.enabled = False cursor = conn.cursor() cursor.execute(""" create table if not exists dummy ( dummy_key VARCHAR(32) PRIMARY KEY, dummy_value TEXT NOT NULL)""") tracer.enabled = True stmt = 'INSERT INTO dummy (dummy_key, dummy_value) VALUES (%s, %s)' data = [ ('foo', 'this is foo'), ('bar', 'this is bar'), ] cursor.executemany(stmt, data) query = 'SELECT dummy_key, dummy_value FROM dummy ORDER BY dummy_key' cursor.execute(query) rows = cursor.fetchall() assert len(rows) == 2 assert rows[0][0] == 'bar' assert rows[0][1] == 'this is bar' assert rows[1][0] == 'foo' assert rows[1][1] == 'this is foo' spans = writer.pop() assert len(spans) == 3 span = spans[1] assert span.get_tag('sql.query') is None cursor.execute('drop table if exists dummy') fetch_span = spans[2] assert fetch_span.name == 'mysql.query.fetchall' def test_query_proc(self): conn, tracer = self._get_conn_tracer() writer = tracer.writer # create a procedure tracer.enabled = False cursor = conn.cursor() cursor.execute('DROP PROCEDURE IF EXISTS sp_sum') cursor.execute(""" CREATE PROCEDURE sp_sum (IN p1 INTEGER, IN p2 INTEGER, OUT p3 INTEGER) BEGIN SET p3 := p1 + p2; END;""") tracer.enabled = True proc = 'sp_sum' data = (40, 2, None) output = cursor.callproc(proc, data) assert len(output) == 3 # resulted p3 isn't stored on output[2], we need to fetch it with select # http://mysqlclient.readthedocs.io/user_guide.html#cursor-objects cursor.execute('SELECT @_sp_sum_2;') assert cursor.fetchone()[0] == 42 spans = writer.pop() assert spans, spans # number of spans depends on MySQL implementation details, # typically, internal calls to execute, but at least we # can expect the next to the last closed span to be our proc. span = spans[-2] assert span.service == self.TEST_SERVICE assert span.name == 'mysql.query' assert span.span_type == 'sql' assert span.error == 0 assert span.get_metric('out.port') == 3306 assert_dict_issuperset(span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) assert span.get_tag('sql.query') is None def test_simple_query_ot(self): """OpenTracing version of test_simple_query.""" conn, tracer = self._get_conn_tracer() writer = tracer.writer ot_tracer = init_tracer('mysql_svc', tracer) with ot_tracer.start_active_span('mysql_op'): cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() assert len(spans) == 2 ot_span, dd_span = spans # confirm parenting assert ot_span.parent_id is None assert dd_span.parent_id == ot_span.span_id assert ot_span.service == 'mysql_svc' assert ot_span.name == 'mysql_op' assert dd_span.service == self.TEST_SERVICE assert dd_span.name == 'mysql.query' assert dd_span.span_type == 'sql' assert dd_span.error == 0 assert dd_span.get_metric('out.port') == 3306 assert_dict_issuperset(dd_span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) def test_simple_query_ot_fetchall(self): """OpenTracing version of test_simple_query.""" with self.override_config('dbapi2', dict(trace_fetch_methods=True)): conn, tracer = self._get_conn_tracer() writer = tracer.writer ot_tracer = init_tracer('mysql_svc', tracer) with ot_tracer.start_active_span('mysql_op'): cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() assert len(spans) == 3 ot_span, dd_span, fetch_span = spans # confirm parenting assert ot_span.parent_id is None assert dd_span.parent_id == ot_span.span_id assert ot_span.service == 'mysql_svc' assert ot_span.name == 'mysql_op' assert dd_span.service == self.TEST_SERVICE assert dd_span.name == 'mysql.query' assert dd_span.span_type == 'sql' assert dd_span.error == 0 assert dd_span.get_metric('out.port') == 3306 assert_dict_issuperset(dd_span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) assert fetch_span.name == 'mysql.query.fetchall' def test_commit(self): conn, tracer = self._get_conn_tracer() writer = tracer.writer conn.commit() spans = writer.pop() assert len(spans) == 1 span = spans[0] assert span.service == self.TEST_SERVICE assert span.name == 'MySQLdb.connection.commit' def test_rollback(self): conn, tracer = self._get_conn_tracer() writer = tracer.writer conn.rollback() spans = writer.pop() assert len(spans) == 1 span = spans[0] assert span.service == self.TEST_SERVICE assert span.name == 'MySQLdb.connection.rollback' def test_analytics_default(self): conn, tracer = self._get_conn_tracer() writer = tracer.writer cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() self.assertEqual(len(spans), 1) span = spans[0] self.assertIsNone(span.get_metric(ANALYTICS_SAMPLE_RATE_KEY)) def test_analytics_with_rate(self): with self.override_config( 'dbapi2', dict(analytics_enabled=True, analytics_sample_rate=0.5) ): conn, tracer = self._get_conn_tracer() writer = tracer.writer cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.get_metric(ANALYTICS_SAMPLE_RATE_KEY), 0.5) def test_analytics_without_rate(self): with self.override_config( 'dbapi2', dict(analytics_enabled=True) ): conn, tracer = self._get_conn_tracer() writer = tracer.writer cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() self.assertEqual(len(spans), 1) span = spans[0] self.assertEqual(span.get_metric(ANALYTICS_SAMPLE_RATE_KEY), 1.0) class TestMysqlPatch(MySQLCore, BaseTracerTestCase): """Ensures MysqlDB is properly patched""" def _connect_with_kwargs(self): return MySQLdb.Connect(**{ 'host': MYSQL_CONFIG['host'], 'user': MYSQL_CONFIG['user'], 'passwd': MYSQL_CONFIG['password'], 'db': MYSQL_CONFIG['database'], 'port': MYSQL_CONFIG['port'], }) def _get_conn_tracer(self): if not self.conn: self.conn = self._connect_with_kwargs() self.conn.ping() # Ensure that the default pin is there, with its default value pin = Pin.get_from(self.conn) assert pin assert pin.service == 'mysql' # Customize the service # we have to apply it on the existing one since new one won't inherit `app` pin.clone(service=self.TEST_SERVICE, tracer=self.tracer).onto(self.conn) return self.conn, self.tracer def _get_conn_tracer_with_positional_args(self): if not self.conn: self.conn = MySQLdb.Connect( MYSQL_CONFIG['host'], MYSQL_CONFIG['user'], MYSQL_CONFIG['password'], MYSQL_CONFIG['database'], MYSQL_CONFIG['port'], ) self.conn.ping() # Ensure that the default pin is there, with its default value pin = Pin.get_from(self.conn) assert pin assert pin.service == 'mysql' # Customize the service # we have to apply it on the existing one since new one won't inherit `app` pin.clone(service=self.TEST_SERVICE, tracer=self.tracer).onto(self.conn) return self.conn, self.tracer def test_patch_unpatch(self): unpatch() # assert we start unpatched conn = self._connect_with_kwargs() assert not Pin.get_from(conn) conn.close() patch() try: writer = self.tracer.writer conn = self._connect_with_kwargs() pin = Pin.get_from(conn) assert pin pin.clone(service=self.TEST_SERVICE, tracer=self.tracer).onto(conn) conn.ping() cursor = conn.cursor() cursor.execute('SELECT 1') rows = cursor.fetchall() assert len(rows) == 1 spans = writer.pop() assert len(spans) == 1 span = spans[0] assert span.service == self.TEST_SERVICE assert span.name == 'mysql.query' assert span.span_type == 'sql' assert span.error == 0 assert span.get_metric('out.port') == 3306 assert_dict_issuperset(span.meta, { 'out.host': u'127.0.0.1', 'db.name': u'test', 'db.user': u'test', }) assert span.get_tag('sql.query') is None finally: unpatch() # assert we finish unpatched conn = self._connect_with_kwargs() assert not Pin.get_from(conn) conn.close() patch()