11from __future__ import annotations
22
33import json
4- from typing import Any , Sequence , TYPE_CHECKING
4+ from collections .abc import Mapping , Sequence
5+ from typing import Any , TYPE_CHECKING
56
67if TYPE_CHECKING :
78 from typing_extensions import TypeAlias
1819CONTENT_TYPE = "application/json"
1920
2021
22+ _VALUE_TYPES = {
23+ # NOTE: order matters, for isinstance(True, int).
24+ bool : ("boolValue" , bool ),
25+ int : ("intValue" , str ),
26+ float : ("doubleValue" , float ),
27+ bytes : ("bytesValue" , bytes ),
28+ str : ("stringValue" , str ),
29+ Sequence : (
30+ "arrayValue" ,
31+ lambda value : {"values" : [_value (e ) for e in _homogeneous_array (value )]},
32+ ),
33+ Mapping : (
34+ "kvlistValue" ,
35+ lambda value : {"values" : [{k : _value (v ) for k , v in value .items ()}]},
36+ ),
37+ }
38+
39+
2140def encode_spans (spans : Sequence [ReadableSpan ]) -> bytes :
2241 spans = sorted (spans , key = lambda s : (id (s .resource ), id (s .instrumentation_scope )))
2342 rv = {"resourceSpans" : []}
@@ -47,36 +66,41 @@ def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
4766
4867
4968def _resource (resource : Resource ):
50- return {
51- "attributes" : [
52- {"key" : k , "value" : _value (v )} for k , v in resource .attributes .items ()
53- ]
54- }
69+ rv = {"attributes" : []}
70+ for k , v in resource .attributes .items ():
71+ try :
72+ rv ["attributes" ].append ({"key" : k , "value" : _value (v )})
73+ except ValueError :
74+ pass
75+
76+ # NOTE: blocks that contain droppedAttributesCount:
77+ # - Event
78+ # - Link
79+ # - InstrumentationScope
80+ # - LogRecord (out of scope for this library)
81+ # - Resource
82+ if dropped := len (resource .attributes ) - len (rv ["attributes" ]):
83+ rv ["dropped_attribute_count" ] = dropped # type: ignore
84+
85+ return rv
86+
87+
88+ def _homogeneous_array (value : list [_LEAF_VALUE ]) -> list [_LEAF_VALUE ]:
89+ # TODO: empty lists are allowed, aren't they?
90+ if len (types := {type (v ) for v in value }) > 1 :
91+ raise ValueError (f"Attribute value arrays must be homogeneous, got { types = } " )
92+ return value
5593
5694
5795def _value (value : _VALUE ) -> dict [str , Any ]:
5896 # Attribute value can be a primitive type, excluging None...
5997 # TODO: protobuf allows bytes, but I think OTLP doesn't.
6098 # TODO: protobuf allows k:v pairs, but I think OTLP doesn't.
61- if isinstance (value , (str , int , float , bool )):
62- k = {
63- # TODO: move these to module level
64- str : "stringValue" ,
65- int : "intValue" ,
66- float : "floatValue" ,
67- bool : "boolValue" ,
68- }[type (value )]
69- return {k : value }
70-
71- # Or a homogenous array of a primitive type, excluding None.
72- value = list (value )
99+ for klass , (key , post ) in _VALUE_TYPES .items ():
100+ if isinstance (value , klass ):
101+ return {key : post (value )}
73102
74- # TODO: empty lists are allowed, aren't they?
75- if len ({type (v ) for v in value }) > 1 :
76- raise ValueError (f"Attribute value arrays must be homogenous, got { value } " )
77-
78- # TODO: maybe prevent recursion, OTEL doesn't allow lists of lists
79- return {"arrayValue" : [_value (e ) for e in value ]}
103+ raise ValueError (f"Cannot convert attribute of { type (value )= } " )
80104
81105
82106def _scope (scope : InstrumentationScope ):
@@ -98,8 +122,22 @@ def _span(span: ReadableSpan):
98122 "endTimeUnixNano" : str (span .end_time ), # -"-
99123 "status" : _status (span .status ),
100124 }
125+
101126 if span .parent :
102127 rv ["parentSpanId" ] = _span_id (span .parent .span_id )
128+
129+ if span .attributes :
130+ rv ["attributes" ] = []
131+
132+ for k , v in span .attributes .items (): # type: ignore
133+ try :
134+ rv ["attributes" ].append ({"key" : k , "value" : _value (v )})
135+ except ValueError :
136+ pass
137+
138+ if dropped := len (span .attributes ) - len (rv .get ("attributes" , ())): # type: ignore
139+ rv ["dropped_attribute_count" ] = dropped # type: ignore
140+
103141 return rv
104142
105143
0 commit comments