diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 55bb418647..c78f69121f 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -193,7 +193,7 @@ def client_response_hook(span: Span, message: dict): import urllib from functools import wraps from timeit import default_timer -from typing import Tuple +from typing import Tuple, List, Dict from asgiref.compatibility import guarantee_single_callable @@ -260,7 +260,62 @@ def get( return decoded def keys(self, carrier: dict) -> typing.List[str]: - return [_key.decode("utf8") for (_key, _value) in carrier] + + """ + In most examples of propogators, they attempt to get a header key with .get() but when that fails they seem to + want to search all keys within carrier. There is not a prescribed structure for carrier in the specs + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md + So we need to evaluate the carrier and peer into any child lists or dicts within carrier and return all of the keys + """ + + + #define the default empty list + returnable = [] + + #internal function that appends keys + def append_keys(key): + if isinstance(key, bytes): + returnable.append(key.decode("utf8")) + elif isinstance(key, str): + returnable.append(key) + + def append_dict_keys(key, val): + #append our current key + append_keys(_key) + + #append all keys within the dict + for x in self.keys(_val): + append_keys(x) + + #carrier is a dict, so iterate over .items() + for _key, _val in carrier.items(): + + #if we have another dict, lets make a recursive call + if isinstance(_val, Dict): + + append_dict_keys(_key, _val) + + # if we have a list, lets iter over that. List can contain tuples(headers) dicts and string so lets approach them all as well + elif isinstance(_val, List): + for list_key in _val: + + #Check for the Tuple + if isinstance(list_key, Tuple): + append_keys(list_key[0]) + + #check for the dict + elif isinstance(list_key, Dict): + + append_dict_keys(_key, _val) + + else: + append_keys(list_key) + + #finally, if our key was just a string, append that + else: + append_keys(_key) + + return returnable asgi_getter = ASGIGetter() diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_getter.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_getter.py index 454162d715..b9ebfeed21 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_getter.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_getter.py @@ -48,3 +48,28 @@ def test_keys(self): getter = ASGIGetter() keys = getter.keys({}) self.assertEqual(keys, []) + + def test_populated_keys(self): + getter = ASGIGetter() + header = { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"Content-Type", b"text/plain"), + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + ( + b"my-custom-regex-header-1", + b"my-custom-regex-value-1,my-custom-regex-value-2", + ), + ( + b"My-Custom-Regex-Header-2", + b"my-custom-regex-value-3,my-custom-regex-value-4", + ), + (b"my-secret-header", b"my-secret-value"), + ], + } + + expected_val = ['type','status','Content-Type', 'custom-test-header-1', 'custom-test-header-2', 'my-custom-regex-header-1', 'My-Custom-Regex-Header-2', 'my-secret-header'] + keys = getter.keys(header) + self.assertEqual(keys, expected_val) \ No newline at end of file