|
31 | 31 | import codecs |
32 | 32 | import gzip |
33 | 33 | import io |
| 34 | +import itertools |
34 | 35 | import json |
35 | 36 | import logging |
36 | 37 | import logging.handlers |
|
40 | 41 | import sys |
41 | 42 | import tempfile |
42 | 43 | import time |
| 44 | +import warnings |
43 | 45 | import zlib |
44 | 46 | from collections import defaultdict |
| 47 | +from typing import Optional |
45 | 48 |
|
46 | 49 | import jsonschema |
47 | 50 | import mock |
|
70 | 73 | METRICSET_SCHEMA = os.path.join(cur_dir, "upstream", "json-specs", "metricset.json") |
71 | 74 | METADATA_SCHEMA = os.path.join(cur_dir, "upstream", "json-specs", "metadata.json") |
72 | 75 |
|
| 76 | +with open(os.path.join(cur_dir, "upstream", "json-specs", "span_types.json")) as f: |
| 77 | + SPAN_TYPES = json.load(f) |
| 78 | + |
73 | 79 |
|
74 | 80 | with codecs.open(ERRORS_SCHEMA, encoding="utf8") as errors_json, codecs.open( |
75 | 81 | TRANSACTIONS_SCHEMA, encoding="utf8" |
|
113 | 119 | } |
114 | 120 |
|
115 | 121 |
|
| 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 | + |
116 | 149 | class ValidatingWSGIApp(ContentServer): |
117 | 150 | def __init__(self, **kwargs): |
118 | 151 | self.skip_validate = kwargs.pop("skip_validate", False) |
@@ -147,6 +180,11 @@ def __call__(self, environ, start_response): |
147 | 180 | except jsonschema.ValidationError as e: |
148 | 181 | fail += 1 |
149 | 182 | 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 |
150 | 188 | code = 202 if not fail else 400 |
151 | 189 | response = Response(status=code) |
152 | 190 | response.headers.clear() |
@@ -199,7 +237,10 @@ def elasticapm_client(request): |
199 | 237 | sys.excepthook = original_exceptionhook |
200 | 238 | execution_context.set_transaction(None) |
201 | 239 | 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 | + ) |
203 | 244 |
|
204 | 245 |
|
205 | 246 | @pytest.fixture() |
@@ -336,6 +377,10 @@ def queue(self, event_type, data, flush=False): |
336 | 377 | validator.validate(data) |
337 | 378 | except jsonschema.ValidationError as e: |
338 | 379 | 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) |
339 | 384 |
|
340 | 385 | def start_thread(self, pid=None): |
341 | 386 | # don't call the parent method, but the one from ThreadManager |
|
0 commit comments