def setUp(self): self.client_patcher = mock.patch( "opentelemetry.exporter.cloud_trace.TraceServiceClient" ) self.client_patcher.start() self.project_id = "PROJECT" self.attributes_variety_pack = { "str_key": "str_value", "bool_key": False, "double_key": 1.421, "int_key": 123, } self.extracted_attributes_variety_pack = ProtoSpan.Attributes( attribute_map={ "str_key": AttributeValue( string_value=TruncatableString( value="str_value", truncated_byte_count=0 ) ), "bool_key": AttributeValue(bool_value=False), "double_key": AttributeValue( string_value=TruncatableString( value="1.4210", truncated_byte_count=0 ) ), "int_key": AttributeValue(int_value=123), } )
def _extract_attributes( attrs: types.Attributes, num_attrs_limit: int, add_agent_attr: bool = False, ) -> ProtoSpan.Attributes: """Convert span.attributes to dict.""" attributes_dict = BoundedDict(num_attrs_limit) invalid_value_dropped_count = 0 for key, value in attrs.items(): key = _truncate_str(key, 128)[0] value = _format_attribute_value(value) if value: attributes_dict[key] = value else: invalid_value_dropped_count += 1 if add_agent_attr: attributes_dict["g.co/agent"] = _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( _strip_characters( pkg_resources.get_distribution( "opentelemetry-sdk").version), _strip_characters(google_ext_version), )) return ProtoSpan.Attributes( attribute_map=attributes_dict, dropped_attributes_count=attributes_dict.dropped + invalid_value_dropped_count, )
def setUpClass(cls): cls.project_id = "PROJECT" cls.attributes_variety_pack = { "str_key": "str_value", "bool_key": False, "double_key": 1.421, "int_key": 123, } cls.extracted_attributes_variety_pack = ProtoSpan.Attributes( attribute_map={ "str_key": AttributeValue(string_value=TruncatableString( value="str_value", truncated_byte_count=0)), "bool_key": AttributeValue(bool_value=False), "double_key": AttributeValue(string_value=TruncatableString( value="1.4210", truncated_byte_count=0)), "int_key": AttributeValue(int_value=123), }) cls.agent_code = _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( _strip_characters( pkg_resources.get_distribution( "opentelemetry-sdk").version), _strip_characters(google_ext_version), )) cls.example_trace_id = "6e0c63257de34c92bf9efcd03927272e" cls.example_span_id = "95bb5edabd45950f" cls.example_time_in_ns = 1589919268850900051 cls.example_time_stamp = _get_time_from_ns(cls.example_time_in_ns) cls.str_300 = "a" * 300 cls.str_256 = "a" * 256 cls.str_128 = "a" * 128
def _extract_attributes( attrs: types.Attributes, num_attrs_limit: int, add_agent_attr: bool = False, ) -> ProtoSpan.Attributes: """Convert span.attributes to dict.""" attributes_dict = BoundedDict( num_attrs_limit) # type: BoundedDict[str, AttributeValue] invalid_value_dropped_count = 0 for key, value in attrs.items() if attrs else []: key = _truncate_str(key, 128)[0] if key in LABELS_MAPPING: # pylint: disable=consider-using-get key = LABELS_MAPPING[key] value = _format_attribute_value(value) if value: attributes_dict[key] = value else: invalid_value_dropped_count += 1 if add_agent_attr: attributes_dict["g.co/agent"] = _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( _strip_characters( pkg_resources.get_distribution( "opentelemetry-sdk").version), _strip_characters(google_ext_version), )) return ProtoSpan.Attributes( attribute_map=attributes_dict, dropped_attributes_count=attributes_dict. dropped # type: ignore[attr-defined] + invalid_value_dropped_count, )
def test_extract_links(self): self.assertIsNone(_extract_links([])) trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id1 = "95bb5edabd45950f" span_id2 = "b6b86ad2915c9ddc" link1 = Link( context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id1, 16), is_remote=False, ), attributes={}, ) link2 = Link( context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id1, 16), is_remote=False, ), attributes=self.attributes_variety_pack, ) link3 = Link( context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id2, 16), is_remote=False, ), attributes={ "illegal_attr_value": dict(), "int_attr_value": 123 }, ) self.assertEqual( _extract_links([link1, link2, link3]), ProtoSpan.Links(link=[ { "trace_id": trace_id, "span_id": span_id1, "type": "TYPE_UNSPECIFIED", "attributes": ProtoSpan.Attributes(attribute_map={}), }, { "trace_id": trace_id, "span_id": span_id1, "type": "TYPE_UNSPECIFIED", "attributes": self.extracted_attributes_variety_pack, }, { "trace_id": trace_id, "span_id": span_id2, "type": "TYPE_UNSPECIFIED", "attributes": { "attribute_map": { "int_attr_value": AttributeValue(int_value=123) }, "dropped_attributes_count": 1, }, }, ]), )
def test_add_agent_attribute(self): self.assertEqual( _extract_attributes({}, num_attrs_limit=4, add_agent_attr=True), ProtoSpan.Attributes( attribute_map={"g.co/agent": self.agent_code}, dropped_attributes_count=0, ), )
def test_export(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" span_datas = [ Span( name="span_name", context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, ) ] cloud_trace_spans = { "name": "projects/{}/traces/{}/spans/{}".format(self.project_id, trace_id, span_id), "span_id": span_id, "parent_span_id": None, "display_name": TruncatableString(value="span_name", truncated_byte_count=0), "attributes": ProtoSpan.Attributes( attribute_map={ "g.co/agent": _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}" .format( pkg_resources.get_distribution( "opentelemetry-sdk").version, cloud_trace_version, )) }), "links": None, "status": None, "time_events": None, "start_time": None, "end_time": None, } client = mock.Mock() exporter = CloudTraceSpanExporter(self.project_id, client=client) exporter.export(span_datas) client.create_span.assert_called_with(**cloud_trace_spans) self.assertTrue(client.create_span.called)
def test_extract_attributes(self): self.assertEqual(_extract_attributes({}, 4), ProtoSpan.Attributes(attribute_map={})) self.assertEqual( _extract_attributes(self.attributes_variety_pack, 4), self.extracted_attributes_variety_pack, ) # Test ignoring attributes with illegal value type self.assertEqual( _extract_attributes({"illegal_attribute_value": dict()}, 4), ProtoSpan.Attributes(attribute_map={}, dropped_attributes_count=1), ) too_many_attrs = {} for attr_key in range(5): too_many_attrs[str(attr_key)] = 0 proto_attrs = _extract_attributes(too_many_attrs, 4) self.assertEqual(proto_attrs.dropped_attributes_count, 1)
def test_extract_label_mapping_attributes(self): attributes_labels_mapping = { "http.scheme": "http", "http.host": "172.19.0.4:8000", "http.method": "POST", "http.request_content_length": 321, "http.response_content_length": 123, "http.route": "/fuzzy/search", "http.status_code": 200, "http.url": "http://172.19.0.4:8000/fuzzy/search", "http.user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", } extracted_attributes_labels_mapping = ProtoSpan.Attributes( attribute_map={ "/http/client_protocol": AttributeValue(string_value=TruncatableString( value="http", truncated_byte_count=0)), "/http/host": AttributeValue(string_value=TruncatableString( value="172.19.0.4:8000", truncated_byte_count=0)), "/http/method": AttributeValue(string_value=TruncatableString( value="POST", truncated_byte_count=0)), "/http/request/size": AttributeValue(int_value=321), "/http/response/size": AttributeValue(int_value=123), "/http/route": AttributeValue(string_value=TruncatableString( value="/fuzzy/search", truncated_byte_count=0)), "/http/status_code": AttributeValue(int_value=200), "/http/url": AttributeValue(string_value=TruncatableString( value="http://172.19.0.4:8000/fuzzy/search", truncated_byte_count=0, )), "/http/user_agent": AttributeValue(string_value=TruncatableString( value= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", truncated_byte_count=0, )), }) self.assertEqual( _extract_attributes(attributes_labels_mapping, num_attrs_limit=9), extracted_attributes_labels_mapping, )
def test_attribute_key_truncation(self): self.assertEqual( _extract_attributes({self.str_300: "attr_value"}, num_attrs_limit=4), ProtoSpan.Attributes( attribute_map={ self.str_128: AttributeValue(string_value=TruncatableString( value="attr_value", truncated_byte_count=0)) }), )
def test_ignore_invalid_attributes(self): self.assertEqual( _extract_attributes( {"illegal_attribute_value": {}, "legal_attribute": 3}, num_attrs_limit=4, ), ProtoSpan.Attributes( attribute_map={"legal_attribute": AttributeValue(int_value=3)}, dropped_attributes_count=1, ), )
def test_agent_attribute_priority(self): # Drop existing attributes in favor of the agent attribute self.assertEqual( _extract_attributes( {"attribute_key": "attr_value"}, num_attrs_limit=1, add_agent_attr=True, ), ProtoSpan.Attributes( attribute_map={"g.co/agent": self.agent_code}, dropped_attributes_count=1, ), )
def _extract_attributes(attrs: types.Attributes, num_attrs_limit: int) -> ProtoSpan.Attributes: """Convert span.attributes to dict.""" attributes_dict = BoundedDict(num_attrs_limit) for key, value in attrs.items(): key = _truncate_str(key, 128)[0] value = _format_attribute_value(value) if value is not None: attributes_dict[key] = value return ProtoSpan.Attributes( attribute_map=attributes_dict, dropped_attributes_count=len(attrs) - len(attributes_dict), )
def test_extract_events(self): self.assertIsNone(_extract_events([])) time_in_ns1 = 1589919268850900051 time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} time_in_ns2 = 1589919438550020326 time_in_ms_and_ns2 = {"seconds": 1589919438, "nanos": 550020352} event1 = Event( name="event1", attributes=self.attributes_variety_pack, timestamp=time_in_ns1, ) event2 = Event( name="event2", attributes={"illegal_attr_value": dict()}, timestamp=time_in_ns2, ) self.assertEqual( _extract_events([event1, event2]), ProtoSpan.TimeEvents( time_event=[ { "time": time_in_ms_and_ns1, "annotation": { "description": TruncatableString( value="event1", truncated_byte_count=0 ), "attributes": self.extracted_attributes_variety_pack, }, }, { "time": time_in_ms_and_ns2, "annotation": { "description": TruncatableString( value="event2", truncated_byte_count=0 ), "attributes": ProtoSpan.Attributes( attribute_map={}, dropped_attributes_count=1 ), }, }, ] ), )
def test_extract_link_with_none_attribute(self): link = Link( context=SpanContext( trace_id=int(self.example_trace_id, 16), span_id=int(self.example_span_id, 16), is_remote=False, ), attributes=None, ) self.assertEqual( _extract_links([link]), ProtoSpan.Links(link=[ { "trace_id": self.example_trace_id, "span_id": self.example_span_id, "type": "TYPE_UNSPECIFIED", "attributes": ProtoSpan.Attributes(attribute_map={}), }, ]), )
def test_extract_multiple_events(self): event1 = Event( name="event1", attributes=self.attributes_variety_pack, timestamp=self.example_time_in_ns, ) event2_nanos = 1589919438550020326 event2 = Event( name="event2", attributes={"illegal_attr_value": dict()}, timestamp=event2_nanos, ) self.assertEqual( _extract_events([event1, event2]), ProtoSpan.TimeEvents( time_event=[ { "time": self.example_time_stamp, "annotation": { "description": TruncatableString( value="event1", truncated_byte_count=0 ), "attributes": self.extracted_attributes_variety_pack, }, }, { "time": _get_time_from_ns(event2_nanos), "annotation": { "description": TruncatableString( value="event2", truncated_byte_count=0 ), "attributes": ProtoSpan.Attributes( attribute_map={}, dropped_attributes_count=1 ), }, }, ] ), )
def test_extract_link_with_none_attribute(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" link = Link( context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), attributes=None, ) self.assertEqual( _extract_links([link]), ProtoSpan.Links( link=[ { "trace_id": trace_id, "span_id": span_id, "type": "TYPE_UNSPECIFIED", "attributes": ProtoSpan.Attributes(attribute_map={}), }, ] ), )
def test_export(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" resource_info = Resource( { "cloud.account.id": 123, "host.id": "host", "cloud.zone": "US", "cloud.provider": "gcp", "gcp.resource_type": "gce_instance", } ) span_datas = [ Span( name="span_name", context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, resource=resource_info, attributes={"attr_key": "attr_value"}, ) ] cloud_trace_spans = { "name": "projects/{}/traces/{}/spans/{}".format( self.project_id, trace_id, span_id ), "span_id": span_id, "parent_span_id": None, "display_name": TruncatableString( value="span_name", truncated_byte_count=0 ), "attributes": ProtoSpan.Attributes( attribute_map={ "g.co/r/gce_instance/zone": _format_attribute_value("US"), "g.co/r/gce_instance/instance_id": _format_attribute_value( "host" ), "g.co/r/gce_instance/project_id": _format_attribute_value( "123" ), "g.co/agent": _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( _strip_characters( pkg_resources.get_distribution( "opentelemetry-sdk" ).version ), _strip_characters(cloud_trace_version), ) ), "attr_key": _format_attribute_value("attr_value"), } ), "links": None, "status": None, "time_events": None, "start_time": None, "end_time": None, } client = mock.Mock() exporter = CloudTraceSpanExporter(self.project_id, client=client) exporter.export(span_datas) self.assertTrue(client.batch_write_spans.called) client.batch_write_spans.assert_called_with( "projects/{}".format(self.project_id), [cloud_trace_spans] )
def test_truncate(self): """Cloud Trace API imposes limits on the length of many things, e.g. strings, number of events, number of attributes. We truncate these things before sending it to the API as an optimization. """ str_300 = "a" * 300 str_256 = "a" * 256 str_128 = "a" * 128 self.assertEqual(_truncate_str("aaaa", 1), ("a", 3)) self.assertEqual(_truncate_str("aaaa", 5), ("aaaa", 0)) self.assertEqual(_truncate_str("aaaa", 4), ("aaaa", 0)) self.assertEqual(_truncate_str("中文翻译", 4), ("中", 9)) self.assertEqual( _format_attribute_value(str_300), AttributeValue( string_value=TruncatableString( value=str_256, truncated_byte_count=300 - 256 ) ), ) self.assertEqual( _extract_attributes({str_300: str_300}, 4), ProtoSpan.Attributes( attribute_map={ str_128: AttributeValue( string_value=TruncatableString( value=str_256, truncated_byte_count=300 - 256 ) ) } ), ) time_in_ns1 = 1589919268850900051 time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} event1 = Event(name=str_300, attributes={}, timestamp=time_in_ns1) self.assertEqual( _extract_events([event1]), ProtoSpan.TimeEvents( time_event=[ { "time": time_in_ms_and_ns1, "annotation": { "description": TruncatableString( value=str_256, truncated_byte_count=300 - 256 ), "attributes": {}, }, }, ] ), ) trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" link = Link( context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), attributes={}, ) too_many_links = [link] * (MAX_NUM_LINKS + 1) self.assertEqual( _extract_links(too_many_links), ProtoSpan.Links( link=[ { "trace_id": trace_id, "span_id": span_id, "type": "TYPE_UNSPECIFIED", "attributes": {}, } ] * MAX_NUM_LINKS, dropped_links_count=len(too_many_links) - MAX_NUM_LINKS, ), ) link_attrs = {} for attr_key in range(MAX_LINK_ATTRS + 1): link_attrs[str(attr_key)] = 0 attr_link = Link( context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), attributes=link_attrs, ) proto_link = _extract_links([attr_link]) self.assertEqual( len(proto_link.link[0].attributes.attribute_map), MAX_LINK_ATTRS ) too_many_events = [event1] * (MAX_NUM_EVENTS + 1) self.assertEqual( _extract_events(too_many_events), ProtoSpan.TimeEvents( time_event=[ { "time": time_in_ms_and_ns1, "annotation": { "description": TruncatableString( value=str_256, truncated_byte_count=300 - 256 ), "attributes": {}, }, }, ] * MAX_NUM_EVENTS, dropped_annotations_count=len(too_many_events) - MAX_NUM_EVENTS, ), ) time_in_ns1 = 1589919268850900051 event_attrs = {} for attr_key in range(MAX_EVENT_ATTRS + 1): event_attrs[str(attr_key)] = 0 proto_events = _extract_events( [Event(name="a", attributes=event_attrs, timestamp=time_in_ns1)] ) self.assertEqual( len( proto_events.time_event[0].annotation.attributes.attribute_map ), MAX_EVENT_ATTRS, )
def test_export(self): resource_info = Resource({ "cloud.account.id": 123, "host.id": "host", "cloud.zone": "US", "cloud.provider": "gcp", "gcp.resource_type": "gce_instance", }) span_datas = [ Span( name="span_name", context=SpanContext( trace_id=int(self.example_trace_id, 16), span_id=int(self.example_span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, resource=resource_info, attributes={"attr_key": "attr_value"}, ) ] cloud_trace_spans = { "name": "projects/{}/traces/{}/spans/{}".format(self.project_id, self.example_trace_id, self.example_span_id), "span_id": self.example_span_id, "parent_span_id": None, "display_name": TruncatableString(value="span_name", truncated_byte_count=0), "attributes": ProtoSpan.Attributes( attribute_map={ "g.co/r/gce_instance/zone": _format_attribute_value("US"), "g.co/r/gce_instance/instance_id": _format_attribute_value("host"), "g.co/r/gce_instance/project_id": _format_attribute_value("123"), "g.co/agent": self.agent_code, "attr_key": _format_attribute_value("attr_value"), }), "links": None, "status": Status(code=StatusCode.UNSET.value), "time_events": None, "start_time": None, "end_time": None, # pylint: disable=no-member "span_kind": ProtoSpan.SpanKind.INTERNAL, } client = mock.Mock() exporter = CloudTraceSpanExporter(self.project_id, client=client) exporter.export(span_datas) self.assertTrue(client.batch_write_spans.called) client.batch_write_spans.assert_called_with( "projects/{}".format(self.project_id), [cloud_trace_spans])
def test_extract_empty_attributes(self): self.assertEqual( _extract_attributes({}, num_attrs_limit=4), ProtoSpan.Attributes(attribute_map={}), )