diff --git a/CHANGELOG.md b/CHANGELOG.md index 239f527d4..470cacf92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## Unreleased +- Added `http code` to `grpc code` status code mapping on `utils` + ([#746](/~https://github.com/census-instrumentation/opencensus-python/pull/746)) ## 0.7.1 Released 2019-08-05 diff --git a/contrib/opencensus-ext-requests/CHANGELOG.md b/contrib/opencensus-ext-requests/CHANGELOG.md index db729e0f5..d0ed6146d 100644 --- a/contrib/opencensus-ext-requests/CHANGELOG.md +++ b/contrib/opencensus-ext-requests/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## Unreleased +- Added attributes following specs listed [here](/~https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/HTTP.md#attributes) + ([#746](/~https://github.com/census-instrumentation/opencensus-python/pull/746)) +- Fixed span name + ([#746](/~https://github.com/census-instrumentation/opencensus-python/pull/746)) ## 0.7.1 Released 2019-08-06 diff --git a/contrib/opencensus-ext-requests/opencensus/ext/requests/trace.py b/contrib/opencensus-ext-requests/opencensus/ext/requests/trace.py index a26e98082..821c816b5 100644 --- a/contrib/opencensus-ext-requests/opencensus/ext/requests/trace.py +++ b/contrib/opencensus-ext-requests/opencensus/ext/requests/trace.py @@ -21,6 +21,7 @@ from urlparse import urlparse from opencensus.trace import attributes_helper +from opencensus.trace import exceptions_status from opencensus.trace import execution_context from opencensus.trace import span as span_module from opencensus.trace import utils @@ -33,8 +34,12 @@ SESSION_WRAP_METHODS = 'request' SESSION_CLASS_NAME = 'Session' -HTTP_URL = attributes_helper.COMMON_ATTRIBUTES['HTTP_URL'] +HTTP_HOST = attributes_helper.COMMON_ATTRIBUTES['HTTP_HOST'] +HTTP_METHOD = attributes_helper.COMMON_ATTRIBUTES['HTTP_METHOD'] +HTTP_PATH = attributes_helper.COMMON_ATTRIBUTES['HTTP_PATH'] +HTTP_ROUTE = attributes_helper.COMMON_ATTRIBUTES['HTTP_ROUTE'] HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES['HTTP_STATUS_CODE'] +HTTP_URL = attributes_helper.COMMON_ATTRIBUTES['HTTP_URL'] def trace_integration(tracer=None): @@ -74,22 +79,47 @@ def call(url, *args, **kwargs): if utils.disable_tracing_hostname(dest_url, blacklist_hostnames): return requests_func(url, *args, **kwargs) + path = parsed_url.path if parsed_url.path else '/' + _tracer = execution_context.get_opencensus_tracer() _span = _tracer.start_span() - _span.name = '[requests]{}'.format(requests_func.__name__) + _span.name = '{}'.format(path) _span.span_kind = span_module.SpanKind.CLIENT - # Add the requests url to attributes - _tracer.add_attribute_to_current_span(HTTP_URL, url) + # Add the requests host to attributes + _tracer.add_attribute_to_current_span( + HTTP_HOST, dest_url) - result = requests_func(url, *args, **kwargs) + # Add the requests method to attributes + _tracer.add_attribute_to_current_span( + HTTP_METHOD, requests_func.__name__.upper()) - # Add the status code to attributes + # Add the requests path to attributes _tracer.add_attribute_to_current_span( - HTTP_STATUS_CODE, str(result.status_code)) + HTTP_PATH, path) - _tracer.end_span() - return result + # Add the requests url to attributes + _tracer.add_attribute_to_current_span(HTTP_URL, url) + + try: + result = requests_func(url, *args, **kwargs) + except requests.Timeout: + _span.set_status(exceptions_status.TIMEOUT) + except requests.URLRequired: + _span.set_status(exceptions_status.INVALID_URL) + except Exception as e: + _span.set_status(exceptions_status.unknown(e)) + else: + # Add the status code to attributes + _tracer.add_attribute_to_current_span( + HTTP_STATUS_CODE, result.status_code + ) + _span.set_status( + utils.status_from_http_code(result.status_code) + ) + return result + finally: + _tracer.end_span() return call @@ -113,10 +143,12 @@ def wrap_session_request(wrapped, instance, args, kwargs): if utils.disable_tracing_hostname(dest_url, blacklist_hostnames): return wrapped(*args, **kwargs) + path = parsed_url.path if parsed_url.path else '/' + _tracer = execution_context.get_opencensus_tracer() _span = _tracer.start_span() - _span.name = '[requests]{}'.format(method) + _span.name = '{}'.format(path) _span.span_kind = span_module.SpanKind.CLIENT try: @@ -127,14 +159,37 @@ def wrap_session_request(wrapped, instance, args, kwargs): except Exception: # pragma: NO COVER pass - # Add the requests url to attributes - _tracer.add_attribute_to_current_span(HTTP_URL, url) + # Add the requests host to attributes + _tracer.add_attribute_to_current_span( + HTTP_HOST, dest_url) - result = wrapped(*args, **kwargs) + # Add the requests method to attributes + _tracer.add_attribute_to_current_span( + HTTP_METHOD, method.upper()) - # Add the status code to attributes + # Add the requests path to attributes _tracer.add_attribute_to_current_span( - HTTP_STATUS_CODE, str(result.status_code)) + HTTP_PATH, path) + + # Add the requests url to attributes + _tracer.add_attribute_to_current_span(HTTP_URL, url) - _tracer.end_span() - return result + try: + result = wrapped(*args, **kwargs) + except requests.Timeout: + _span.set_status(exceptions_status.TIMEOUT) + except requests.URLRequired: + _span.set_status(exceptions_status.INVALID_URL) + except Exception as e: + _span.set_status(exceptions_status.unknown(e)) + else: + # Add the status code to attributes + _tracer.add_attribute_to_current_span( + HTTP_STATUS_CODE, result.status_code + ) + _span.set_status( + utils.status_from_http_code(result.status_code) + ) + return result + finally: + _tracer.end_span() diff --git a/contrib/opencensus-ext-requests/tests/test_requests_trace.py b/contrib/opencensus-ext-requests/tests/test_requests_trace.py index 44e0abf56..67222c618 100644 --- a/contrib/opencensus-ext-requests/tests/test_requests_trace.py +++ b/contrib/opencensus-ext-requests/tests/test_requests_trace.py @@ -15,10 +15,12 @@ import unittest import mock +import requests from opencensus.trace.tracers import noop_tracer from opencensus.ext.requests import trace from opencensus.trace import span as span_module, execution_context +from opencensus.trace import status as status_module class Test_requests_trace(unittest.TestCase): @@ -95,19 +97,30 @@ def test_wrap_requests(self): wrapped = trace.wrap_requests(mock_func) - url = 'http://localhost:8080' + url = 'http://localhost:8080/test' with patch, patch_thread: wrapped(url) - expected_attributes = {'http.url': url, 'http.status_code': '200'} - expected_name = '[requests]get' + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'GET', + 'http.path': '/test', + 'http.status_code': 200, + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(0) self.assertEqual(span_module.SpanKind.CLIENT, mock_tracer.current_span.span_kind) self.assertEqual(expected_attributes, mock_tracer.current_span.attributes) self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) def test_wrap_requests_blacklist_ok(self): mock_return = mock.Mock() @@ -138,7 +151,7 @@ def test_wrap_requests_blacklist_ok(self): with patch_tracer, patch_attr, patch_thread: wrapped(url) - expected_name = '[requests]get' + expected_name = '/' self.assertEqual(expected_name, mock_tracer.current_span.name) @@ -204,6 +217,144 @@ def test_wrap_requests_exporter_thread(self): self.assertEqual(None, mock_tracer.current_span) + def test_wrap_requests_timeout(self): + mock_return = mock.Mock() + mock_return.status_code = 200 + return_value = mock_return + mock_func = mock.Mock() + mock_func.__name__ = 'get' + mock_func.return_value = return_value + mock_func.side_effect = requests.Timeout + mock_tracer = MockTracer() + + patch = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'get_opencensus_tracer', + return_value=mock_tracer) + + patch_thread = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'is_exporter', + return_value=False) + + wrapped = trace.wrap_requests(mock_func) + + url = 'http://localhost:8080/test' + + with patch, patch_thread: + wrapped(url) + + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'GET', + 'http.path': '/test', + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(4, 'request timed out') + + self.assertEqual(span_module.SpanKind.CLIENT, + mock_tracer.current_span.span_kind) + self.assertEqual(expected_attributes, + mock_tracer.current_span.attributes) + self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) + + def test_wrap_requests_invalid_url(self): + mock_return = mock.Mock() + mock_return.status_code = 200 + return_value = mock_return + mock_func = mock.Mock() + mock_func.__name__ = 'get' + mock_func.return_value = return_value + mock_func.side_effect = requests.URLRequired + mock_tracer = MockTracer() + + patch = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'get_opencensus_tracer', + return_value=mock_tracer) + + patch_thread = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'is_exporter', + return_value=False) + + wrapped = trace.wrap_requests(mock_func) + + url = 'http://localhost:8080/test' + + with patch, patch_thread: + wrapped(url) + + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'GET', + 'http.path': '/test', + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(3, 'invalid URL') + + self.assertEqual(span_module.SpanKind.CLIENT, + mock_tracer.current_span.span_kind) + self.assertEqual(expected_attributes, + mock_tracer.current_span.attributes) + self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) + + def test_wrap_requests_exception(self): + mock_return = mock.Mock() + mock_return.status_code = 200 + return_value = mock_return + mock_func = mock.Mock() + mock_func.__name__ = 'get' + mock_func.return_value = return_value + mock_func.side_effect = requests.TooManyRedirects + mock_tracer = MockTracer() + + patch = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'get_opencensus_tracer', + return_value=mock_tracer) + + patch_thread = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'is_exporter', + return_value=False) + + wrapped = trace.wrap_requests(mock_func) + + url = 'http://localhost:8080/test' + + with patch, patch_thread: + wrapped(url) + + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'GET', + 'http.path': '/test', + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(2, '') + + self.assertEqual(span_module.SpanKind.CLIENT, + mock_tracer.current_span.span_kind) + self.assertEqual(expected_attributes, + mock_tracer.current_span.attributes) + self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) + def test_wrap_session_request(self): wrapped = mock.Mock(return_value=mock.Mock(status_code=200)) @@ -220,7 +371,7 @@ def test_wrap_session_request(self): 'is_exporter', return_value=False) - url = 'http://localhost:8080' + url = 'http://localhost:8080/test' request_method = 'POST' kwargs = {} @@ -228,8 +379,15 @@ def test_wrap_session_request(self): trace.wrap_session_request(wrapped, 'Session.request', (request_method, url), kwargs) - expected_attributes = {'http.url': url, 'http.status_code': '200'} - expected_name = '[requests]POST' + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'POST', + 'http.path': '/test', + 'http.status_code': 200, + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(0) self.assertEqual(span_module.SpanKind.CLIENT, mock_tracer.current_span.span_kind) @@ -237,6 +395,10 @@ def test_wrap_session_request(self): mock_tracer.current_span.attributes) self.assertEqual(kwargs['headers']['x-trace'], 'some-value') self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) def test_wrap_session_request_blacklist_ok(self): def wrapped(*args, **kwargs): @@ -261,14 +423,14 @@ def wrapped(*args, **kwargs): 'is_exporter', return_value=False) - url = 'http://localhost' + url = 'http://localhost/' request_method = 'POST' with patch_tracer, patch_attr, patch_thread: trace.wrap_session_request(wrapped, 'Session.request', (request_method, url), {}) - expected_name = '[requests]POST' + expected_name = '/' self.assertEqual(expected_name, mock_tracer.current_span.name) def test_wrap_session_request_blacklist_nok(self): @@ -406,6 +568,141 @@ def test_tracer_headers_are_overwritten(self): self.assertEqual(kwargs['headers']['x-trace'], 'some-value') + def test_wrap_session_request_timeout(self): + wrapped = mock.Mock(return_value=mock.Mock(status_code=200)) + wrapped.side_effect = requests.Timeout + + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda x: {'x-trace': 'some-value'})) + + patch = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'get_opencensus_tracer', + return_value=mock_tracer) + patch_thread = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'is_exporter', + return_value=False) + + url = 'http://localhost:8080/test' + request_method = 'POST' + kwargs = {} + + with patch, patch_thread: + trace.wrap_session_request(wrapped, 'Session.request', + (request_method, url), kwargs) + + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'POST', + 'http.path': '/test', + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(4, 'request timed out') + + self.assertEqual(span_module.SpanKind.CLIENT, + mock_tracer.current_span.span_kind) + self.assertEqual(expected_attributes, + mock_tracer.current_span.attributes) + self.assertEqual(kwargs['headers']['x-trace'], 'some-value') + self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) + + def test_wrap_session_request_invalid_url(self): + wrapped = mock.Mock(return_value=mock.Mock(status_code=200)) + wrapped.side_effect = requests.URLRequired + + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda x: {'x-trace': 'some-value'})) + + patch = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'get_opencensus_tracer', + return_value=mock_tracer) + patch_thread = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'is_exporter', + return_value=False) + + url = 'http://localhost:8080/test' + request_method = 'POST' + kwargs = {} + + with patch, patch_thread: + trace.wrap_session_request(wrapped, 'Session.request', + (request_method, url), kwargs) + + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'POST', + 'http.path': '/test', + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(3, 'invalid URL') + + self.assertEqual(span_module.SpanKind.CLIENT, + mock_tracer.current_span.span_kind) + self.assertEqual(expected_attributes, + mock_tracer.current_span.attributes) + self.assertEqual(kwargs['headers']['x-trace'], 'some-value') + self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) + + def test_wrap_session_request_exception(self): + wrapped = mock.Mock(return_value=mock.Mock(status_code=200)) + wrapped.side_effect = requests.TooManyRedirects + + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda x: {'x-trace': 'some-value'})) + + patch = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'get_opencensus_tracer', + return_value=mock_tracer) + patch_thread = mock.patch( + 'opencensus.ext.requests.trace.execution_context.' + 'is_exporter', + return_value=False) + + url = 'http://localhost:8080/test' + request_method = 'POST' + kwargs = {} + + with patch, patch_thread: + trace.wrap_session_request(wrapped, 'Session.request', + (request_method, url), kwargs) + + expected_attributes = { + 'http.host': 'localhost:8080', + 'http.method': 'POST', + 'http.path': '/test', + 'http.url': url, + } + expected_name = '/test' + expected_status = status_module.Status(2, '') + + self.assertEqual(span_module.SpanKind.CLIENT, + mock_tracer.current_span.span_kind) + self.assertEqual(expected_attributes, + mock_tracer.current_span.attributes) + self.assertEqual(kwargs['headers']['x-trace'], 'some-value') + self.assertEqual(expected_name, mock_tracer.current_span.name) + self.assertEqual( + expected_status.__dict__, + mock_tracer.current_span.status.__dict__ + ) + class MockTracer(object): def __init__(self, propagator=None): @@ -414,8 +711,7 @@ def __init__(self, propagator=None): self.propagator = propagator def start_span(self): - span = mock.Mock() - span.attributes = {} + span = MockSpan() self.current_span = span return span @@ -424,3 +720,11 @@ def end_span(self): def add_attribute_to_current_span(self, key, value): self.current_span.attributes[key] = value + + +class MockSpan(object): + def __init__(self): + self.attributes = {} + + def set_status(self, status): + self.status = status diff --git a/opencensus/trace/exceptions_status.py b/opencensus/trace/exceptions_status.py new file mode 100644 index 000000000..a57bdec60 --- /dev/null +++ b/opencensus/trace/exceptions_status.py @@ -0,0 +1,24 @@ +# Copyright 2017, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.rpc import code_pb2 +from opencensus.trace.status import Status + +CANCELLED = Status(code_pb2.CANCELLED) +INVALID_URL = Status(code_pb2.INVALID_ARGUMENT, message='invalid URL') +TIMEOUT = Status(code_pb2.DEADLINE_EXCEEDED, message='request timed out') + + +def unknown(exception): + return Status.from_exception(exception) diff --git a/opencensus/trace/utils.py b/opencensus/trace/utils.py index 1a39c8c0e..93add22d5 100644 --- a/opencensus/trace/utils.py +++ b/opencensus/trace/utils.py @@ -14,7 +14,9 @@ import re +from google.rpc import code_pb2 from opencensus.trace import execution_context +from opencensus.trace.status import Status # By default the blacklist urls are not tracing, currently just include the # health check url. The paths are literal string matched instead of regular @@ -93,3 +95,33 @@ def disable_tracing_hostname(url, blacklist_hostnames=None): blacklist_hostnames = [] return url in blacklist_hostnames + + +def status_from_http_code(http_code): + """Returns equivalent status from http status code + based on OpenCensus specs. + + :type http_code: int + :param http_code: HTTP request status code. + + :rtype: int + :returns: A instance of :class: `~opencensus.trace.status.Status`. + """ + if http_code <= 199: + return Status(code_pb2.UNKNOWN) + + if http_code <= 399: + return Status(code_pb2.OK) + + grpc_code = { + 400: code_pb2.INVALID_ARGUMENT, + 401: code_pb2.UNAUTHENTICATED, + 403: code_pb2.PERMISSION_DENIED, + 404: code_pb2.NOT_FOUND, + 429: code_pb2.RESOURCE_EXHAUSTED, + 501: code_pb2.UNIMPLEMENTED, + 503: code_pb2.UNAVAILABLE, + 504: code_pb2.DEADLINE_EXCEEDED, + }.get(http_code, code_pb2.UNKNOWN) + + return Status(grpc_code) diff --git a/tests/unit/trace/test_exceptions_status.py b/tests/unit/trace/test_exceptions_status.py new file mode 100644 index 000000000..1b3f7e963 --- /dev/null +++ b/tests/unit/trace/test_exceptions_status.py @@ -0,0 +1,49 @@ +# Copyright 2017, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from google.rpc import code_pb2 +from opencensus.trace import exceptions_status + + +class TestUtils(unittest.TestCase): + def test_cancelled(self): + self.assertEqual( + exceptions_status.CANCELLED.canonical_code, + code_pb2.CANCELLED + ) + + def test_invalid_url(self): + self.assertEqual( + exceptions_status.INVALID_URL.canonical_code, + code_pb2.INVALID_ARGUMENT + ) + + def test_timeout(self): + self.assertEqual( + exceptions_status.TIMEOUT.canonical_code, + code_pb2.DEADLINE_EXCEEDED + ) + + def test_unknown(self): + status = exceptions_status.unknown(Exception) + self.assertEqual( + status.canonical_code, + code_pb2.UNKNOWN + ) + self.assertEqual( + status.description, + str(Exception) + ) diff --git a/tests/unit/trace/test_ext_utils.py b/tests/unit/trace/test_ext_utils.py index 96092fcb5..a7d946e49 100644 --- a/tests/unit/trace/test_ext_utils.py +++ b/tests/unit/trace/test_ext_utils.py @@ -16,6 +16,7 @@ import mock +from google.rpc import code_pb2 from opencensus.trace import utils @@ -73,3 +74,67 @@ def test_disable_tracing_hostname_explicit(self): url = '127.0.0.1:80' disable_tracing = utils.disable_tracing_hostname(url, blacklist_paths) self.assertFalse(disable_tracing) + + def test_grpc_code_from_http_code(self): + test_cases = [ + { + 'http_code': 0, + 'grpc_code': code_pb2.UNKNOWN, + }, + { + 'http_code': 200, + 'grpc_code': code_pb2.OK, + }, + { + 'http_code': 399, + 'grpc_code': code_pb2.OK, + }, + { + 'http_code': 400, + 'grpc_code': code_pb2.INVALID_ARGUMENT, + }, + { + 'http_code': 504, + 'grpc_code': code_pb2.DEADLINE_EXCEEDED, + }, + { + 'http_code': 404, + 'grpc_code': code_pb2.NOT_FOUND, + }, + { + 'http_code': 403, + 'grpc_code': code_pb2.PERMISSION_DENIED, + }, + { + 'http_code': 401, + 'grpc_code': code_pb2.UNAUTHENTICATED, + }, + { + 'http_code': 429, + 'grpc_code': code_pb2.RESOURCE_EXHAUSTED, + }, + { + 'http_code': 501, + 'grpc_code': code_pb2.UNIMPLEMENTED, + }, + { + 'http_code': 503, + 'grpc_code': code_pb2.UNAVAILABLE, + }, + { + 'http_code': 600, + 'grpc_code': code_pb2.UNKNOWN, + }, + ] + + for test_case in test_cases: + status = utils.status_from_http_code(test_case['http_code']) + self.assertEqual( + status.canonical_code, + test_case['grpc_code'], + 'HTTP: {} / GRPC: expected = {}, actual = {}'.format( + test_case['http_code'], + test_case['grpc_code'], + status.canonical_code, + ) + )