Skip to content

Commit 1654dd5

Browse files
feat(dbapi2): include object name in procedure call spans (#1938)
* feat(dbapi2): include object name in procedure call spans Include the name of the procedure in the name of the span. Examples: CALL procedure() EXECUTE procedure Issue: #1937 * feat(dbapi2): use span action exec for procedure calls Set span action to exec instead of query for procedure calls not using callproc(). Issue: #1937
1 parent 8ee719b commit 1654dd5

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

elasticapm/instrumentation/packages/dbapi2.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,30 +170,46 @@ def extract_signature(sql):
170170
keyword = "INTO" if sql_type == "INSERT" else "FROM"
171171
sql_type = sql_type + " " + keyword
172172

173-
table_name = look_for_table(sql, keyword)
173+
object_name = look_for_table(sql, keyword)
174174
elif sql_type in ["CREATE", "DROP"]:
175175
# 2nd word is part of SQL type
176176
sql_type = sql_type + sql[first_space:second_space]
177-
table_name = ""
177+
object_name = ""
178178
elif sql_type == "UPDATE":
179-
table_name = look_for_table(sql, "UPDATE")
179+
object_name = look_for_table(sql, "UPDATE")
180180
elif sql_type == "SELECT":
181181
# Name is first table
182182
try:
183183
sql_type = "SELECT FROM"
184-
table_name = look_for_table(sql, "FROM")
184+
object_name = look_for_table(sql, "FROM")
185185
except Exception:
186-
table_name = ""
186+
object_name = ""
187+
elif sql_type in ["EXEC", "EXECUTE"]:
188+
sql_type = "EXECUTE"
189+
end = second_space if second_space > first_space else len(sql)
190+
object_name = sql[first_space + 1 : end]
191+
elif sql_type == "CALL":
192+
first_paren = sql.find("(", first_space)
193+
end = first_paren if first_paren > first_space else len(sql)
194+
procedure_name = sql[first_space + 1 : end].rstrip(";")
195+
object_name = procedure_name + "()"
187196
else:
188197
# No name
189-
table_name = ""
198+
object_name = ""
190199

191-
signature = " ".join(filter(bool, [sql_type, table_name]))
200+
signature = " ".join(filter(bool, [sql_type, object_name]))
192201
return signature
193202

194203

195204
QUERY_ACTION = "query"
196205
EXEC_ACTION = "exec"
206+
PROCEDURE_STATEMENTS = ["EXEC", "EXECUTE", "CALL"]
207+
208+
209+
def extract_action_from_signature(signature, default):
210+
if signature.split(" ")[0] in PROCEDURE_STATEMENTS:
211+
return EXEC_ACTION
212+
return default
197213

198214

199215
class CursorProxy(wrapt.ObjectProxy):
@@ -226,6 +242,7 @@ def _trace_sql(self, method, sql, params, action=QUERY_ACTION):
226242
signature = sql_string + "()"
227243
else:
228244
signature = self.extract_signature(sql_string)
245+
action = extract_action_from_signature(signature, action)
229246

230247
# Truncate sql_string to 10000 characters to prevent large queries from
231248
# causing an error to APM server.

tests/instrumentation/dbapi2_tests.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@
2929
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030
import pytest
3131

32-
from elasticapm.instrumentation.packages.dbapi2 import Literal, extract_signature, scan, tokenize
32+
from elasticapm.instrumentation.packages.dbapi2 import (
33+
Literal,
34+
extract_action_from_signature,
35+
extract_signature,
36+
scan,
37+
tokenize,
38+
)
3339

3440

3541
def test_scan_simple():
@@ -114,3 +120,37 @@ def test_extract_signature_bytes():
114120
actual = extract_signature(sql)
115121
expected = "HELLO"
116122
assert actual == expected
123+
124+
125+
@pytest.mark.parametrize(
126+
["sql", "expected"],
127+
[
128+
(
129+
"EXEC AdventureWorks2022.dbo.uspGetEmployeeManagers 50;",
130+
"EXECUTE AdventureWorks2022.dbo.uspGetEmployeeManagers",
131+
),
132+
("EXECUTE sp_who2", "EXECUTE sp_who2"),
133+
("EXEC sp_updatestats @@all_schemas = 'true'", "EXECUTE sp_updatestats"),
134+
("CALL get_car_stats_by_year(2017, @number, @min, @avg, @max);", "CALL get_car_stats_by_year()"),
135+
("CALL get_car_stats_by_year", "CALL get_car_stats_by_year()"),
136+
("CALL get_car_stats_by_year;", "CALL get_car_stats_by_year()"),
137+
("CALL get_car_stats_by_year();", "CALL get_car_stats_by_year()"),
138+
],
139+
)
140+
def test_extract_signature_for_procedure_call(sql, expected):
141+
actual = extract_signature(sql)
142+
assert actual == expected
143+
144+
145+
@pytest.mark.parametrize(
146+
["sql", "expected"],
147+
[
148+
("SELECT FROM table", "query"),
149+
("EXEC sp_who", "exec"),
150+
("EXECUTE sp_updatestats", "exec"),
151+
("CALL me_maybe", "exec"),
152+
],
153+
)
154+
def test_extract_action_from_signature(sql, expected):
155+
actual = extract_action_from_signature(sql, "query")
156+
assert actual == expected

0 commit comments

Comments
 (0)