Skip to content

Commit 6097d44

Browse files
authored
validate JSON types/subtypes (#1647)
as our default is `code.custom`, which is not yet part of the spec, we get a lot of warnings with this. Closes #1305
1 parent 26c2844 commit 6097d44

File tree

4 files changed

+113
-3
lines changed

4 files changed

+113
-3
lines changed

elasticapm/instrumentation/packages/asyncio/aiopg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def call(self, module, method, wrapped, instance, args, kwargs):
5858
else:
5959
raise AssertionError("call from uninstrumented method")
6060
async with async_capture_span(
61-
name, leaf=True, span_type="db", span_subtype="postgres", span_action=action, extra=context
61+
name, leaf=True, span_type="db", span_subtype="postgresql", span_action=action, extra=context
6262
):
6363
return await wrapped(*args, **kwargs)
6464

tests/fixtures.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import codecs
3232
import gzip
3333
import io
34+
import itertools
3435
import json
3536
import logging
3637
import logging.handlers
@@ -40,8 +41,10 @@
4041
import sys
4142
import tempfile
4243
import time
44+
import warnings
4345
import zlib
4446
from collections import defaultdict
47+
from typing import Optional
4548

4649
import jsonschema
4750
import mock
@@ -70,6 +73,9 @@
7073
METRICSET_SCHEMA = os.path.join(cur_dir, "upstream", "json-specs", "metricset.json")
7174
METADATA_SCHEMA = os.path.join(cur_dir, "upstream", "json-specs", "metadata.json")
7275

76+
with open(os.path.join(cur_dir, "upstream", "json-specs", "span_types.json")) as f:
77+
SPAN_TYPES = json.load(f)
78+
7379

7480
with codecs.open(ERRORS_SCHEMA, encoding="utf8") as errors_json, codecs.open(
7581
TRANSACTIONS_SCHEMA, encoding="utf8"
@@ -113,6 +119,33 @@
113119
}
114120

115121

122+
def validate_span_type_subtype(item: dict) -> Optional[str]:
123+
"""
124+
Validate span type/subtype against spec.
125+
126+
At first, only warnings are issued. At a later point, it should return the message as string
127+
which will cause a validation error.
128+
"""
129+
if item["type"] not in SPAN_TYPES:
130+
warnings.warn(f"Span type \"{item['type']}\" not found in JSON spec", UserWarning)
131+
return
132+
span_type = SPAN_TYPES[item["type"]]
133+
subtypes = span_type.get("subtypes", [])
134+
if not subtypes and item["subtype"] and not span_type.get("allow_unlisted_subtype", False):
135+
warnings.warn(
136+
f"Span type \"{item['type']}\" has no subtypes, but subtype \"{item['subtype']}\" is set", UserWarning
137+
)
138+
return
139+
if item["subtype"] not in SPAN_TYPES[item["type"]].get("subtypes", []):
140+
if not SPAN_TYPES[item["type"]].get("allow_unlisted_subtype", False):
141+
warnings.warn(f"Subtype \"{item['subtype']}\" not allowed for span type \"{item['type']}\"", UserWarning)
142+
return
143+
else:
144+
if "python" not in subtypes.get(item["subtype"], {}).get("__used_by", []):
145+
warnings.warn(f"\"{item['type']}.{item['subtype']}\" not marked as used by Python", UserWarning)
146+
return None
147+
148+
116149
class ValidatingWSGIApp(ContentServer):
117150
def __init__(self, **kwargs):
118151
self.skip_validate = kwargs.pop("skip_validate", False)
@@ -147,6 +180,11 @@ def __call__(self, environ, start_response):
147180
except jsonschema.ValidationError as e:
148181
fail += 1
149182
content += "/".join(map(str, e.absolute_schema_path)) + ": " + e.message + "\n"
183+
if item_type == "span":
184+
result = validate_span_type_subtype(item)
185+
if result:
186+
fail += 1
187+
content += result
150188
code = 202 if not fail else 400
151189
response = Response(status=code)
152190
response.headers.clear()
@@ -199,7 +237,10 @@ def elasticapm_client(request):
199237
sys.excepthook = original_exceptionhook
200238
execution_context.set_transaction(None)
201239
execution_context.unset_span(clear_all=True)
202-
assert not client._transport.validation_errors
240+
if client._transport.validation_errors:
241+
pytest.fail(
242+
"Validation errors:" + "\n".join(*itertools.chain(v for v in client._transport.validation_errors.values()))
243+
)
203244

204245

205246
@pytest.fixture()
@@ -336,6 +377,10 @@ def queue(self, event_type, data, flush=False):
336377
validator.validate(data)
337378
except jsonschema.ValidationError as e:
338379
self.validation_errors[event_type].append(e.message)
380+
if event_type == "span":
381+
result = validate_span_type_subtype(data)
382+
if result:
383+
self.validation_errors[event_type].append(result)
339384

340385
def start_thread(self, pid=None):
341386
# don't call the parent method, but the one from ThreadManager

tests/instrumentation/asyncio_tests/aiopg_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ async def test_select_sleep(instrument, cursor, elasticapm_client):
9494
assert 100 < span["duration"] < 110
9595
assert transaction["id"] == span["transaction_id"]
9696
assert span["type"] == "db"
97-
assert span["subtype"] == "postgres"
97+
assert span["subtype"] == "postgresql"
9898
assert span["action"] == "query"
9999
assert span["sync"] == False
100100

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2022, 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+
import pytest
32+
33+
import elasticapm
34+
35+
36+
def test_span_type_not_found(elasticapm_client):
37+
elasticapm_client.begin_transaction("test")
38+
with pytest.warns(UserWarning, match='Span type "bar" not found in JSON spec'):
39+
with elasticapm.capture_span("foo", span_type="bar"):
40+
pass
41+
elasticapm_client.end_transaction("test")
42+
43+
44+
def test_span_type_no_subtypes(elasticapm_client):
45+
elasticapm_client.begin_transaction("test")
46+
with pytest.warns(UserWarning, match='Span type "process" has no subtypes, but subtype "foo" is set'):
47+
with elasticapm.capture_span("foo", span_type="process", span_subtype="foo"):
48+
pass
49+
elasticapm_client.end_transaction("test")
50+
51+
52+
def test_span_type_subtype_not_allowed(elasticapm_client):
53+
elasticapm_client.begin_transaction("test")
54+
with pytest.warns(UserWarning, match='Subtype "anonexistingdb" not allowed for span type "db"'):
55+
with elasticapm.capture_span("foo", span_type="db", span_subtype="anonexistingdb"):
56+
pass
57+
elasticapm_client.end_transaction("test")
58+
59+
60+
def test_span_type_not_used_by_python(elasticapm_client):
61+
elasticapm_client.begin_transaction("test")
62+
with pytest.warns(UserWarning, match='"json.parse" not marked as used by Python'):
63+
with elasticapm.capture_span("foo", span_type="json", span_subtype="parse"):
64+
pass
65+
elasticapm_client.end_transaction("test")

0 commit comments

Comments
 (0)