We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
dbapi2 fails to extract table name when using square brackets 
**Describe the bug**: ...

Queries made to tables which requires escaping end up with the wrong span name.

The following spans are SELECTs from four different tables, but only two unique span names appear.
![Screenshot 2023-10-31 at 19 13 46](https://github.com/elastic/apm-agent-python/assets/435885/5384fd4d-df13-4828-a9f2-b7e1c309f07f)

**To Reproduce**

Import package and call [extract_signature](https://github.com/elastic/apm-agent-python/blob/05332cd007560615b4421b1567659ff9f9634088/elasticapm/instrumentation/packages/dbapi2.py#L153):

```python
>>> from elasticapm.instrumentation.packages import dbapi2
>>> dbapi2.extract_signature("SELECT username FROM user")
'SELECT FROM user'
>>> dbapi2.extract_signature("SELECT username FROM [user]")
'SELECT FROM ['
```

**Environment (please complete the following information)**
- OS: [e.g. Linux]
- Python version:
- Framework and version [e.g. Django 2.1]:
- APM Server version:
- Agent version:


**Additional context**

Add any other context about the problem here.

- Agent config options <!-- be careful not to post sensitive information -->
  <details>
    <summary>Click to expand</summary>

    ```
    replace this line with your agent config options
    remember to mask any sensitive fields like tokens
    ```
  </details>
- `requirements.txt`:
  <details>
    <summary>Click to expand</summary>

    ```
    replace this line with your `requirements.txt`
    ```
  </details>


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `elasticapm/instrumentation/packages/dbapi2.py`
Content:
```
1 #  BSD 3-Clause License
2 #
3 #  Copyright (c) 2019, Elasticsearch BV
4 #  All rights reserved.
5 #
6 #  Redistribution and use in source and binary forms, with or without
7 #  modification, are permitted provided that the following conditions are met:
8 #
9 #  * Redistributions of source code must retain the above copyright notice, this
10 #    list of conditions and the following disclaimer.
11 #
12 #  * Redistributions in binary form must reproduce the above copyright notice,
13 #    this list of conditions and the following disclaimer in the documentation
14 #    and/or other materials provided with the distribution.
15 #
16 #  * Neither the name of the copyright holder nor the names of its
17 #    contributors may be used to endorse or promote products derived from
18 #    this software without specific prior written permission.
19 #
20 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 #  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 #  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 #  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 #  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 
31 """Provides classes to instrument dbapi2 providers
32 
33 https://www.python.org/dev/peps/pep-0249/
34 """
35 
36 import re
37 
38 import wrapt
39 
40 from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
41 from elasticapm.traces import capture_span
42 from elasticapm.utils.encoding import force_text, shorten
43 
44 
45 class Literal(object):
46     def __init__(self, literal_type, content) -> None:
47         self.literal_type = literal_type
48         self.content = content
49 
50     def __eq__(self, other):
51         return isinstance(other, Literal) and self.literal_type == other.literal_type and self.content == other.content
52 
53     def __repr__(self):
54         return "<Literal {}{}{}>".format(self.literal_type, self.content, self.literal_type)
55 
56 
57 def look_for_table(sql, keyword):
58     tokens = tokenize(sql)
59     table_name = _scan_for_table_with_tokens(tokens, keyword)
60     if isinstance(table_name, Literal):
61         table_name = table_name.content.strip(table_name.literal_type)
62     return table_name
63 
64 
65 def _scan_for_table_with_tokens(tokens, keyword):
66     seen_keyword = False
67     for idx, lexeme in scan(tokens):
68         if seen_keyword:
69             if lexeme == "(":
70                 return _scan_for_table_with_tokens(tokens[idx:], keyword)
71             else:
72                 return lexeme
73 
74         if isinstance(lexeme, str) and lexeme.upper() == keyword:
75             seen_keyword = True
76 
77 
78 def tokenize(sql):
79     # split on anything that is not a word character, excluding dots
80     return [t for t in re.split(r"([^\w.])", sql) if t != ""]
81 
82 
83 def scan(tokens):
84     literal_start_idx = None
85     literal_started = None
86     prev_was_escape = False
87     lexeme = []
88 
89     i = 0
90     while i < len(tokens):
91         token = tokens[i]
92         if literal_start_idx:
93             if prev_was_escape:
94                 prev_was_escape = False
95                 lexeme.append(token)
96             else:
97                 if token == literal_started:
98                     if literal_started == "'" and len(tokens) > i + 1 and tokens[i + 1] == "'":  # double quotes
99                         i += 1
100                         lexeme.append("'")
101                     else:
102                         yield i, Literal(literal_started, "".join(lexeme))
103                         literal_start_idx = None
104                         literal_started = None
105                         lexeme = []
106                 else:
107                     if token == "\\":
108                         prev_was_escape = token
109                     else:
110                         prev_was_escape = False
111                         lexeme.append(token)
112         elif literal_start_idx is None:
113             if token in ["'", '"', "`"]:
114                 literal_start_idx = i
115                 literal_started = token
116             elif token == "$":
117                 # Postgres can use arbitrary characters between two $'s as a
118                 # literal separation token, e.g.: $fish$ literal $fish$
119                 # This part will detect that and skip over the literal.
120                 try:
121                     # Closing dollar of the opening quote,
122                     # i.e. the second $ in the first $fish$
123                     closing_dollar_idx = tokens.index("$", i + 1)
124                 except ValueError:
125                     pass
126                 else:
127                     quote = tokens[i : closing_dollar_idx + 1]
128                     length = len(quote)
129                     # Opening dollar of the closing quote,
130                     # i.e. the first $ in the second $fish$
131                     closing_quote_idx = closing_dollar_idx + 1
132                     while True:
133                         try:
134                             closing_quote_idx = tokens.index("$", closing_quote_idx)
135                         except ValueError:
136                             break
137                         if tokens[closing_quote_idx : closing_quote_idx + length] == quote:
138                             yield i, Literal(
139                                 "".join(quote), "".join(tokens[closing_dollar_idx + 1 : closing_quote_idx])
140                             )
141                             i = closing_quote_idx + length
142                             break
143                         closing_quote_idx += 1
144             else:
145                 if token != " ":
146                     yield i, token
147         i += 1
148 
149     if lexeme:
150         yield i, lexeme
151 
152 
153 def extract_signature(sql):
154     """
155     Extracts a minimal signature from a given SQL query
156     :param sql: the SQL statement
157     :return: a string representing the signature
158     """
159     sql = force_text(sql)
160     sql = sql.strip()
161     first_space = sql.find(" ")
162     if first_space < 0:
163         return sql
164 
165     second_space = sql.find(" ", first_space + 1)
166 
167     sql_type = sql[0:first_space].upper()
168 
169     if sql_type in ["INSERT", "DELETE"]:
170         keyword = "INTO" if sql_type == "INSERT" else "FROM"
171         sql_type = sql_type + " " + keyword
172 
173         object_name = look_for_table(sql, keyword)
174     elif sql_type in ["CREATE", "DROP"]:
175         # 2nd word is part of SQL type
176         sql_type = sql_type + sql[first_space:second_space]
177         object_name = ""
178     elif sql_type == "UPDATE":
179         object_name = look_for_table(sql, "UPDATE")
180     elif sql_type == "SELECT":
181         # Name is first table
182         try:
183             sql_type = "SELECT FROM"
184             object_name = look_for_table(sql, "FROM")
185         except Exception:
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 + "()"
196     else:
197         # No name
198         object_name = ""
199 
200     signature = " ".join(filter(bool, [sql_type, object_name]))
201     return signature
202 
203 
204 QUERY_ACTION = "query"
205 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
213 
214 
215 class CursorProxy(wrapt.ObjectProxy):
216     provider_name = None
217     DML_QUERIES = ("INSERT", "DELETE", "UPDATE")
218 
219     def __init__(self, wrapped, destination_info=None) -> None:
220         super(CursorProxy, self).__init__(wrapped)
221         self._self_destination_info = destination_info or {}
222 
223     def callproc(self, procname, params=None):
224         return self._trace_sql(self.__wrapped__.callproc, procname, params, action=EXEC_ACTION)
225 
226     def execute(self, sql, params=None):
227         return self._trace_sql(self.__wrapped__.execute, sql, params)
228 
229     def executemany(self, sql, param_list):
230         return self._trace_sql(self.__wrapped__.executemany, sql, param_list)
231 
232     def _bake_sql(self, sql):
233         """
234         Method to turn the "sql" argument into a string. Most database backends simply return
235         the given object, as it is already a string
236         """
237         return sql
238 
239     def _trace_sql(self, method, sql, params, action=QUERY_ACTION):
240         sql_string = self._bake_sql(sql)
241         if action == EXEC_ACTION:
242             signature = sql_string + "()"
243         else:
244             signature = self.extract_signature(sql_string)
245             action = extract_action_from_signature(signature, action)
246 
247         # Truncate sql_string to 10000 characters to prevent large queries from
248         # causing an error to APM server.
249         sql_string = shorten(sql_string, string_length=10000)
250 
251         with capture_span(
252             signature,
253             span_type="db",
254             span_subtype=self.provider_name,
255             span_action=action,
256             extra={
257                 "db": {"type": "sql", "statement": sql_string, "instance": getattr(self, "_self_database", None)},
258                 "destination": self._self_destination_info,
259             },
260             skip_frames=1,
261             leaf=True,
262         ) as span:
263             if params is None:
264                 result = method(sql)
265             else:
266                 result = method(sql, params)
267             # store "rows affected", but only for DML queries like insert/update/delete
268             if span and self.rowcount not in (-1, None) and signature.startswith(self.DML_QUERIES):
269                 span.update_context("db", {"rows_affected": self.rowcount})
270             return result
271 
272     def extract_signature(self, sql):
273         raise NotImplementedError()
274 
275 
276 class ConnectionProxy(wrapt.ObjectProxy):
277     cursor_proxy = CursorProxy
278 
279     def __init__(self, wrapped, destination_info=None) -> None:
280         super(ConnectionProxy, self).__init__(wrapped)
281         self._self_destination_info = destination_info
282 
283     def cursor(self, *args, **kwargs):
284         return self.cursor_proxy(self.__wrapped__.cursor(*args, **kwargs), self._self_destination_info)
285 
286 
287 class DbApi2Instrumentation(AbstractInstrumentedModule):
288     connect_method = None
289 
290     def call(self, module, method, wrapped, instance, args, kwargs):
291         return ConnectionProxy(wrapped(*args, **kwargs))
292 
293     def call_if_sampling(self, module, method, wrapped, instance, args, kwargs):
294         # Contrasting to the superclass implementation, we *always* want to
295         # return a proxied connection, even if there is no ongoing elasticapm
296         # transaction yet. This ensures that we instrument the cursor once
297         # the transaction started.
298         return self.call(module, method, wrapped, instance, args, kwargs)
299
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/elasticapm/instrumentation/packages/dbapi2.py b/elasticapm/instrumentation/packages/dbapi2.py
--- a/elasticapm/instrumentation/packages/dbapi2.py
+++ b/elasticapm/instrumentation/packages/dbapi2.py
@@ -76,8 +76,8 @@
 
 
 def tokenize(sql):
-    # split on anything that is not a word character, excluding dots
-    return [t for t in re.split(r"([^\w.])", sql) if t != ""]
+    # split on anything that is not a word character or a square bracket, excluding dots
+    return [t for t in re.split(r"([^\w.\[\]])", sql) if t != ""]
 
 
 def scan(tokens):

{"golden_diff": "diff --git a/elasticapm/instrumentation/packages/dbapi2.py b/elasticapm/instrumentation/packages/dbapi2.py\n--- a/elasticapm/instrumentation/packages/dbapi2.py\n+++ b/elasticapm/instrumentation/packages/dbapi2.py\n@@ -76,8 +76,8 @@\n \n \n def tokenize(sql):\n-    # split on anything that is not a word character, excluding dots\n-    return [t for t in re.split(r\"([^\\w.])\", sql) if t != \"\"]\n+    # split on anything that is not a word character or a square bracket, excluding dots\n+    return [t for t in re.split(r\"([^\\w.\\[\\]])\", sql) if t != \"\"]\n \n \n def scan(tokens):\n", "issue": "dbapi2 fails to extract table name when using square brackets \n**Describe the bug**: ...\r\n\r\nQueries made to tables which requires escaping end up with the wrong span name.\r\n\r\nThe following spans are SELECTs from four different tables, but only two unique span names appear.\r\n![Screenshot 2023-10-31 at 19 13 46](https://github.com/elastic/apm-agent-python/assets/435885/5384fd4d-df13-4828-a9f2-b7e1c309f07f)\r\n\r\n**To Reproduce**\r\n\r\nImport package and call [extract_signature](https://github.com/elastic/apm-agent-python/blob/05332cd007560615b4421b1567659ff9f9634088/elasticapm/instrumentation/packages/dbapi2.py#L153):\r\n\r\n```python\r\n>>> from elasticapm.instrumentation.packages import dbapi2\r\n>>> dbapi2.extract_signature(\"SELECT username FROM user\")\r\n'SELECT FROM user'\r\n>>> dbapi2.extract_signature(\"SELECT username FROM [user]\")\r\n'SELECT FROM ['\r\n```\r\n\r\n**Environment (please complete the following information)**\r\n- OS: [e.g. Linux]\r\n- Python version:\r\n- Framework and version [e.g. Django 2.1]:\r\n- APM Server version:\r\n- Agent version:\r\n\r\n\r\n**Additional context**\r\n\r\nAdd any other context about the problem here.\r\n\r\n- Agent config options <!-- be careful not to post sensitive information -->\r\n  <details>\r\n    <summary>Click to expand</summary>\r\n\r\n    ```\r\n    replace this line with your agent config options\r\n    remember to mask any sensitive fields like tokens\r\n    ```\r\n  </details>\r\n- `requirements.txt`:\r\n  <details>\r\n    <summary>Click to expand</summary>\r\n\r\n    ```\r\n    replace this line with your `requirements.txt`\r\n    ```\r\n  </details>\r\n\n", "before_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides classes to instrument dbapi2 providers\n\nhttps://www.python.org/dev/peps/pep-0249/\n\"\"\"\n\nimport re\n\nimport wrapt\n\nfrom elasticapm.instrumentation.packages.base import AbstractInstrumentedModule\nfrom elasticapm.traces import capture_span\nfrom elasticapm.utils.encoding import force_text, shorten\n\n\nclass Literal(object):\n    def __init__(self, literal_type, content) -> None:\n        self.literal_type = literal_type\n        self.content = content\n\n    def __eq__(self, other):\n        return isinstance(other, Literal) and self.literal_type == other.literal_type and self.content == other.content\n\n    def __repr__(self):\n        return \"<Literal {}{}{}>\".format(self.literal_type, self.content, self.literal_type)\n\n\ndef look_for_table(sql, keyword):\n    tokens = tokenize(sql)\n    table_name = _scan_for_table_with_tokens(tokens, keyword)\n    if isinstance(table_name, Literal):\n        table_name = table_name.content.strip(table_name.literal_type)\n    return table_name\n\n\ndef _scan_for_table_with_tokens(tokens, keyword):\n    seen_keyword = False\n    for idx, lexeme in scan(tokens):\n        if seen_keyword:\n            if lexeme == \"(\":\n                return _scan_for_table_with_tokens(tokens[idx:], keyword)\n            else:\n                return lexeme\n\n        if isinstance(lexeme, str) and lexeme.upper() == keyword:\n            seen_keyword = True\n\n\ndef tokenize(sql):\n    # split on anything that is not a word character, excluding dots\n    return [t for t in re.split(r\"([^\\w.])\", sql) if t != \"\"]\n\n\ndef scan(tokens):\n    literal_start_idx = None\n    literal_started = None\n    prev_was_escape = False\n    lexeme = []\n\n    i = 0\n    while i < len(tokens):\n        token = tokens[i]\n        if literal_start_idx:\n            if prev_was_escape:\n                prev_was_escape = False\n                lexeme.append(token)\n            else:\n                if token == literal_started:\n                    if literal_started == \"'\" and len(tokens) > i + 1 and tokens[i + 1] == \"'\":  # double quotes\n                        i += 1\n                        lexeme.append(\"'\")\n                    else:\n                        yield i, Literal(literal_started, \"\".join(lexeme))\n                        literal_start_idx = None\n                        literal_started = None\n                        lexeme = []\n                else:\n                    if token == \"\\\\\":\n                        prev_was_escape = token\n                    else:\n                        prev_was_escape = False\n                        lexeme.append(token)\n        elif literal_start_idx is None:\n            if token in [\"'\", '\"', \"`\"]:\n                literal_start_idx = i\n                literal_started = token\n            elif token == \"$\":\n                # Postgres can use arbitrary characters between two $'s as a\n                # literal separation token, e.g.: $fish$ literal $fish$\n                # This part will detect that and skip over the literal.\n                try:\n                    # Closing dollar of the opening quote,\n                    # i.e. the second $ in the first $fish$\n                    closing_dollar_idx = tokens.index(\"$\", i + 1)\n                except ValueError:\n                    pass\n                else:\n                    quote = tokens[i : closing_dollar_idx + 1]\n                    length = len(quote)\n                    # Opening dollar of the closing quote,\n                    # i.e. the first $ in the second $fish$\n                    closing_quote_idx = closing_dollar_idx + 1\n                    while True:\n                        try:\n                            closing_quote_idx = tokens.index(\"$\", closing_quote_idx)\n                        except ValueError:\n                            break\n                        if tokens[closing_quote_idx : closing_quote_idx + length] == quote:\n                            yield i, Literal(\n                                \"\".join(quote), \"\".join(tokens[closing_dollar_idx + 1 : closing_quote_idx])\n                            )\n                            i = closing_quote_idx + length\n                            break\n                        closing_quote_idx += 1\n            else:\n                if token != \" \":\n                    yield i, token\n        i += 1\n\n    if lexeme:\n        yield i, lexeme\n\n\ndef extract_signature(sql):\n    \"\"\"\n    Extracts a minimal signature from a given SQL query\n    :param sql: the SQL statement\n    :return: a string representing the signature\n    \"\"\"\n    sql = force_text(sql)\n    sql = sql.strip()\n    first_space = sql.find(\" \")\n    if first_space < 0:\n        return sql\n\n    second_space = sql.find(\" \", first_space + 1)\n\n    sql_type = sql[0:first_space].upper()\n\n    if sql_type in [\"INSERT\", \"DELETE\"]:\n        keyword = \"INTO\" if sql_type == \"INSERT\" else \"FROM\"\n        sql_type = sql_type + \" \" + keyword\n\n        object_name = look_for_table(sql, keyword)\n    elif sql_type in [\"CREATE\", \"DROP\"]:\n        # 2nd word is part of SQL type\n        sql_type = sql_type + sql[first_space:second_space]\n        object_name = \"\"\n    elif sql_type == \"UPDATE\":\n        object_name = look_for_table(sql, \"UPDATE\")\n    elif sql_type == \"SELECT\":\n        # Name is first table\n        try:\n            sql_type = \"SELECT FROM\"\n            object_name = look_for_table(sql, \"FROM\")\n        except Exception:\n            object_name = \"\"\n    elif sql_type in [\"EXEC\", \"EXECUTE\"]:\n        sql_type = \"EXECUTE\"\n        end = second_space if second_space > first_space else len(sql)\n        object_name = sql[first_space + 1 : end]\n    elif sql_type == \"CALL\":\n        first_paren = sql.find(\"(\", first_space)\n        end = first_paren if first_paren > first_space else len(sql)\n        procedure_name = sql[first_space + 1 : end].rstrip(\";\")\n        object_name = procedure_name + \"()\"\n    else:\n        # No name\n        object_name = \"\"\n\n    signature = \" \".join(filter(bool, [sql_type, object_name]))\n    return signature\n\n\nQUERY_ACTION = \"query\"\nEXEC_ACTION = \"exec\"\nPROCEDURE_STATEMENTS = [\"EXEC\", \"EXECUTE\", \"CALL\"]\n\n\ndef extract_action_from_signature(signature, default):\n    if signature.split(\" \")[0] in PROCEDURE_STATEMENTS:\n        return EXEC_ACTION\n    return default\n\n\nclass CursorProxy(wrapt.ObjectProxy):\n    provider_name = None\n    DML_QUERIES = (\"INSERT\", \"DELETE\", \"UPDATE\")\n\n    def __init__(self, wrapped, destination_info=None) -> None:\n        super(CursorProxy, self).__init__(wrapped)\n        self._self_destination_info = destination_info or {}\n\n    def callproc(self, procname, params=None):\n        return self._trace_sql(self.__wrapped__.callproc, procname, params, action=EXEC_ACTION)\n\n    def execute(self, sql, params=None):\n        return self._trace_sql(self.__wrapped__.execute, sql, params)\n\n    def executemany(self, sql, param_list):\n        return self._trace_sql(self.__wrapped__.executemany, sql, param_list)\n\n    def _bake_sql(self, sql):\n        \"\"\"\n        Method to turn the \"sql\" argument into a string. Most database backends simply return\n        the given object, as it is already a string\n        \"\"\"\n        return sql\n\n    def _trace_sql(self, method, sql, params, action=QUERY_ACTION):\n        sql_string = self._bake_sql(sql)\n        if action == EXEC_ACTION:\n            signature = sql_string + \"()\"\n        else:\n            signature = self.extract_signature(sql_string)\n            action = extract_action_from_signature(signature, action)\n\n        # Truncate sql_string to 10000 characters to prevent large queries from\n        # causing an error to APM server.\n        sql_string = shorten(sql_string, string_length=10000)\n\n        with capture_span(\n            signature,\n            span_type=\"db\",\n            span_subtype=self.provider_name,\n            span_action=action,\n            extra={\n                \"db\": {\"type\": \"sql\", \"statement\": sql_string, \"instance\": getattr(self, \"_self_database\", None)},\n                \"destination\": self._self_destination_info,\n            },\n            skip_frames=1,\n            leaf=True,\n        ) as span:\n            if params is None:\n                result = method(sql)\n            else:\n                result = method(sql, params)\n            # store \"rows affected\", but only for DML queries like insert/update/delete\n            if span and self.rowcount not in (-1, None) and signature.startswith(self.DML_QUERIES):\n                span.update_context(\"db\", {\"rows_affected\": self.rowcount})\n            return result\n\n    def extract_signature(self, sql):\n        raise NotImplementedError()\n\n\nclass ConnectionProxy(wrapt.ObjectProxy):\n    cursor_proxy = CursorProxy\n\n    def __init__(self, wrapped, destination_info=None) -> None:\n        super(ConnectionProxy, self).__init__(wrapped)\n        self._self_destination_info = destination_info\n\n    def cursor(self, *args, **kwargs):\n        return self.cursor_proxy(self.__wrapped__.cursor(*args, **kwargs), self._self_destination_info)\n\n\nclass DbApi2Instrumentation(AbstractInstrumentedModule):\n    connect_method = None\n\n    def call(self, module, method, wrapped, instance, args, kwargs):\n        return ConnectionProxy(wrapped(*args, **kwargs))\n\n    def call_if_sampling(self, module, method, wrapped, instance, args, kwargs):\n        # Contrasting to the superclass implementation, we *always* want to\n        # return a proxied connection, even if there is no ongoing elasticapm\n        # transaction yet. This ensures that we instrument the cursor once\n        # the transaction started.\n        return self.call(module, method, wrapped, instance, args, kwargs)\n", "path": "elasticapm/instrumentation/packages/dbapi2.py"}], "after_files": [{"content": "#  BSD 3-Clause License\n#\n#  Copyright (c) 2019, Elasticsearch BV\n#  All rights reserved.\n#\n#  Redistribution and use in source and binary forms, with or without\n#  modification, are permitted provided that the following conditions are met:\n#\n#  * Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer.\n#\n#  * Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n#\n#  * Neither the name of the copyright holder nor the names of its\n#    contributors may be used to endorse or promote products derived from\n#    this software without specific prior written permission.\n#\n#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Provides classes to instrument dbapi2 providers\n\nhttps://www.python.org/dev/peps/pep-0249/\n\"\"\"\n\nimport re\n\nimport wrapt\n\nfrom elasticapm.instrumentation.packages.base import AbstractInstrumentedModule\nfrom elasticapm.traces import capture_span\nfrom elasticapm.utils.encoding import force_text, shorten\n\n\nclass Literal(object):\n    def __init__(self, literal_type, content) -> None:\n        self.literal_type = literal_type\n        self.content = content\n\n    def __eq__(self, other):\n        return isinstance(other, Literal) and self.literal_type == other.literal_type and self.content == other.content\n\n    def __repr__(self):\n        return \"<Literal {}{}{}>\".format(self.literal_type, self.content, self.literal_type)\n\n\ndef look_for_table(sql, keyword):\n    tokens = tokenize(sql)\n    table_name = _scan_for_table_with_tokens(tokens, keyword)\n    if isinstance(table_name, Literal):\n        table_name = table_name.content.strip(table_name.literal_type)\n    return table_name\n\n\ndef _scan_for_table_with_tokens(tokens, keyword):\n    seen_keyword = False\n    for idx, lexeme in scan(tokens):\n        if seen_keyword:\n            if lexeme == \"(\":\n                return _scan_for_table_with_tokens(tokens[idx:], keyword)\n            else:\n                return lexeme\n\n        if isinstance(lexeme, str) and lexeme.upper() == keyword:\n            seen_keyword = True\n\n\ndef tokenize(sql):\n    # split on anything that is not a word character or a square bracket, excluding dots\n    return [t for t in re.split(r\"([^\\w.\\[\\]])\", sql) if t != \"\"]\n\n\ndef scan(tokens):\n    literal_start_idx = None\n    literal_started = None\n    prev_was_escape = False\n    lexeme = []\n\n    i = 0\n    while i < len(tokens):\n        token = tokens[i]\n        if literal_start_idx:\n            if prev_was_escape:\n                prev_was_escape = False\n                lexeme.append(token)\n            else:\n                if token == literal_started:\n                    if literal_started == \"'\" and len(tokens) > i + 1 and tokens[i + 1] == \"'\":  # double quotes\n                        i += 1\n                        lexeme.append(\"'\")\n                    else:\n                        yield i, Literal(literal_started, \"\".join(lexeme))\n                        literal_start_idx = None\n                        literal_started = None\n                        lexeme = []\n                else:\n                    if token == \"\\\\\":\n                        prev_was_escape = token\n                    else:\n                        prev_was_escape = False\n                        lexeme.append(token)\n        elif literal_start_idx is None:\n            if token in [\"'\", '\"', \"`\"]:\n                literal_start_idx = i\n                literal_started = token\n            elif token == \"$\":\n                # Postgres can use arbitrary characters between two $'s as a\n                # literal separation token, e.g.: $fish$ literal $fish$\n                # This part will detect that and skip over the literal.\n                try:\n                    # Closing dollar of the opening quote,\n                    # i.e. the second $ in the first $fish$\n                    closing_dollar_idx = tokens.index(\"$\", i + 1)\n                except ValueError:\n                    pass\n                else:\n                    quote = tokens[i : closing_dollar_idx + 1]\n                    length = len(quote)\n                    # Opening dollar of the closing quote,\n                    # i.e. the first $ in the second $fish$\n                    closing_quote_idx = closing_dollar_idx + 1\n                    while True:\n                        try:\n                            closing_quote_idx = tokens.index(\"$\", closing_quote_idx)\n                        except ValueError:\n                            break\n                        if tokens[closing_quote_idx : closing_quote_idx + length] == quote:\n                            yield i, Literal(\n                                \"\".join(quote), \"\".join(tokens[closing_dollar_idx + 1 : closing_quote_idx])\n                            )\n                            i = closing_quote_idx + length\n                            break\n                        closing_quote_idx += 1\n            else:\n                if token != \" \":\n                    yield i, token\n        i += 1\n\n    if lexeme:\n        yield i, lexeme\n\n\ndef extract_signature(sql):\n    \"\"\"\n    Extracts a minimal signature from a given SQL query\n    :param sql: the SQL statement\n    :return: a string representing the signature\n    \"\"\"\n    sql = force_text(sql)\n    sql = sql.strip()\n    first_space = sql.find(\" \")\n    if first_space < 0:\n        return sql\n\n    second_space = sql.find(\" \", first_space + 1)\n\n    sql_type = sql[0:first_space].upper()\n\n    if sql_type in [\"INSERT\", \"DELETE\"]:\n        keyword = \"INTO\" if sql_type == \"INSERT\" else \"FROM\"\n        sql_type = sql_type + \" \" + keyword\n\n        object_name = look_for_table(sql, keyword)\n    elif sql_type in [\"CREATE\", \"DROP\"]:\n        # 2nd word is part of SQL type\n        sql_type = sql_type + sql[first_space:second_space]\n        object_name = \"\"\n    elif sql_type == \"UPDATE\":\n        object_name = look_for_table(sql, \"UPDATE\")\n    elif sql_type == \"SELECT\":\n        # Name is first table\n        try:\n            sql_type = \"SELECT FROM\"\n            object_name = look_for_table(sql, \"FROM\")\n        except Exception:\n            object_name = \"\"\n    elif sql_type in [\"EXEC\", \"EXECUTE\"]:\n        sql_type = \"EXECUTE\"\n        end = second_space if second_space > first_space else len(sql)\n        object_name = sql[first_space + 1 : end]\n    elif sql_type == \"CALL\":\n        first_paren = sql.find(\"(\", first_space)\n        end = first_paren if first_paren > first_space else len(sql)\n        procedure_name = sql[first_space + 1 : end].rstrip(\";\")\n        object_name = procedure_name + \"()\"\n    else:\n        # No name\n        object_name = \"\"\n\n    signature = \" \".join(filter(bool, [sql_type, object_name]))\n    return signature\n\n\nQUERY_ACTION = \"query\"\nEXEC_ACTION = \"exec\"\nPROCEDURE_STATEMENTS = [\"EXEC\", \"EXECUTE\", \"CALL\"]\n\n\ndef extract_action_from_signature(signature, default):\n    if signature.split(\" \")[0] in PROCEDURE_STATEMENTS:\n        return EXEC_ACTION\n    return default\n\n\nclass CursorProxy(wrapt.ObjectProxy):\n    provider_name = None\n    DML_QUERIES = (\"INSERT\", \"DELETE\", \"UPDATE\")\n\n    def __init__(self, wrapped, destination_info=None) -> None:\n        super(CursorProxy, self).__init__(wrapped)\n        self._self_destination_info = destination_info or {}\n\n    def callproc(self, procname, params=None):\n        return self._trace_sql(self.__wrapped__.callproc, procname, params, action=EXEC_ACTION)\n\n    def execute(self, sql, params=None):\n        return self._trace_sql(self.__wrapped__.execute, sql, params)\n\n    def executemany(self, sql, param_list):\n        return self._trace_sql(self.__wrapped__.executemany, sql, param_list)\n\n    def _bake_sql(self, sql):\n        \"\"\"\n        Method to turn the \"sql\" argument into a string. Most database backends simply return\n        the given object, as it is already a string\n        \"\"\"\n        return sql\n\n    def _trace_sql(self, method, sql, params, action=QUERY_ACTION):\n        sql_string = self._bake_sql(sql)\n        if action == EXEC_ACTION:\n            signature = sql_string + \"()\"\n        else:\n            signature = self.extract_signature(sql_string)\n            action = extract_action_from_signature(signature, action)\n\n        # Truncate sql_string to 10000 characters to prevent large queries from\n        # causing an error to APM server.\n        sql_string = shorten(sql_string, string_length=10000)\n\n        with capture_span(\n            signature,\n            span_type=\"db\",\n            span_subtype=self.provider_name,\n            span_action=action,\n            extra={\n                \"db\": {\"type\": \"sql\", \"statement\": sql_string, \"instance\": getattr(self, \"_self_database\", None)},\n                \"destination\": self._self_destination_info,\n            },\n            skip_frames=1,\n            leaf=True,\n        ) as span:\n            if params is None:\n                result = method(sql)\n            else:\n                result = method(sql, params)\n            # store \"rows affected\", but only for DML queries like insert/update/delete\n            if span and self.rowcount not in (-1, None) and signature.startswith(self.DML_QUERIES):\n                span.update_context(\"db\", {\"rows_affected\": self.rowcount})\n            return result\n\n    def extract_signature(self, sql):\n        raise NotImplementedError()\n\n\nclass ConnectionProxy(wrapt.ObjectProxy):\n    cursor_proxy = CursorProxy\n\n    def __init__(self, wrapped, destination_info=None) -> None:\n        super(ConnectionProxy, self).__init__(wrapped)\n        self._self_destination_info = destination_info\n\n    def cursor(self, *args, **kwargs):\n        return self.cursor_proxy(self.__wrapped__.cursor(*args, **kwargs), self._self_destination_info)\n\n\nclass DbApi2Instrumentation(AbstractInstrumentedModule):\n    connect_method = None\n\n    def call(self, module, method, wrapped, instance, args, kwargs):\n        return ConnectionProxy(wrapped(*args, **kwargs))\n\n    def call_if_sampling(self, module, method, wrapped, instance, args, kwargs):\n        # Contrasting to the superclass implementation, we *always* want to\n        # return a proxied connection, even if there is no ongoing elasticapm\n        # transaction yet. This ensures that we instrument the cursor once\n        # the transaction started.\n        return self.call(module, method, wrapped, instance, args, kwargs)\n", "path": "elasticapm/instrumentation/packages/dbapi2.py"}]}