Skip to content

Commit cb1a3ce

Browse files
committed
Introduce to_dict
Introduce `to_dict` to the objects included in the existing JSON serialization process for `ReadableSpan`, `MetricsData`, `LogRecord`, and `Resource` objects. This includes adding `to_dict` to objects that are included within the serialized data structures of these objects. In places where `repr()` serialization was used, it has been replaced by a JSON-compatible serialization instead. Inconsistencies between null and empty string values were preserved, but in cases where attributes are optional, an empty dictionary is provided as well to be more consistent with cases where attributes are not optional and an empty dictionary represents no attributes were specified on the containing object. These changes also included: 1. Dictionary typing was included for all the `to_dict` methods for clarity in subsequent usage. 2. `DataT` and `DataPointT` were did not include the exponential histogram types in point.py, and so those were added with new `to_json` and `to_dict` methods as well for consistency. It appears that the exponential types were added later and including them in the types might have been overlooked. Please let me know if that is a misunderstanding on my part. 3. OrderedDict was removed in a number of places associated with the existing `to_json` functionality given its redundancy for Python 3.7+ compatibility. I was assuming this was legacy code for previous compatibility, but please let me know if that's not the case as well. 4. `to_dict` was added to objects like `SpanContext`, `Link`, and `Event` that were previously being serialized by static methods within the `ReadableSpan` class and accessing private/protected members. This simplified the serialization in the `ReadableSpan` class and those methods were removed. However, once again, let me know if there was a larger purpose to those I could not find. Finally, I used `to_dict` as the method names here to be consistent with other related usages. For example, `dataclasses.asdict()`. But, mostly because that was by far the most popular usage within the larger community: 328k files found on GitHub that define `to_dict` functions, which include some of the most popular Python libraries to date: https://github.com/search?q=%22def+to_dict%28%22+language%3APython&type=code&p=1&l=Python versus 3.3k files found on GitHub that define `to_dictionary` functions: https://github.com/search?q=%22def+to_dictionary%28%22+language%3APython&type=code&l=Python However, if there is a preference for this library to use `to_dictionary` instead let me know and I will adjust. Fixes #3364
1 parent 37de27a commit cb1a3ce

File tree

10 files changed

+427
-207
lines changed

10 files changed

+427
-207
lines changed

opentelemetry-api/src/opentelemetry/trace/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
NonRecordingSpan,
104104
Span,
105105
SpanContext,
106+
SpanContextDict,
106107
TraceFlags,
107108
TraceState,
108109
format_span_id,
@@ -130,6 +131,13 @@ def attributes(self) -> types.Attributes:
130131
pass
131132

132133

134+
class LinkDict(typing.TypedDict):
135+
"""Dictionary representation of a span Link."""
136+
137+
context: SpanContextDict
138+
attributes: types.Attributes
139+
140+
133141
class Link(_LinkBase):
134142
"""A link to a `Span`. The attributes of a Link are immutable.
135143
@@ -152,6 +160,12 @@ def __init__(
152160
def attributes(self) -> types.Attributes:
153161
return self._attributes
154162

163+
def to_dict(self) -> LinkDict:
164+
return {
165+
"context": self.context.to_dict(),
166+
"attributes": dict(self._attributes),
167+
}
168+
155169

156170
_Links = Optional[Sequence[Link]]
157171

opentelemetry-api/src/opentelemetry/trace/span.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,14 @@ def values(self) -> typing.ValuesView[str]:
403403
_SPAN_ID_MAX_VALUE = 2**64 - 1
404404

405405

406+
class SpanContextDict(typing.TypedDict):
407+
"""Dictionary representation of a SpanContext."""
408+
409+
trace_id: str
410+
span_id: str
411+
trace_state: typing.Dict[str, str]
412+
413+
406414
class SpanContext(
407415
typing.Tuple[int, int, bool, "TraceFlags", "TraceState", bool]
408416
):
@@ -477,6 +485,13 @@ def trace_state(self) -> "TraceState":
477485
def is_valid(self) -> bool:
478486
return self[5] # pylint: disable=unsubscriptable-object
479487

488+
def to_dict(self) -> SpanContextDict:
489+
return {
490+
"trace_id": f"0x{format_trace_id(self.trace_id)}",
491+
"span_id": f"0x{format_span_id(self.span_id)}",
492+
"trace_state": dict(self.trace_state),
493+
}
494+
480495
def __setattr__(self, *args: str) -> None:
481496
_logger.debug(
482497
"Immutable type, ignoring call to set attribute", stack_info=True

opentelemetry-api/src/opentelemetry/trace/status.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ class StatusCode(enum.Enum):
3232
"""The operation contains an error."""
3333

3434

35+
class StatusDict(typing.TypedDict):
36+
"""Dictionary representation of a trace Status."""
37+
38+
status_code: str
39+
description: typing.Optional[str]
40+
41+
3542
class Status:
3643
"""Represents the status of a finished Span.
3744
@@ -80,3 +87,10 @@ def is_ok(self) -> bool:
8087
def is_unset(self) -> bool:
8188
"""Returns true if unset, false otherwise."""
8289
return self._status_code is StatusCode.UNSET
90+
91+
def to_dict(self) -> StatusDict:
92+
"""Convert to a dictionary representation of the status."""
93+
return {
94+
"status_code": str(self.status_code.name),
95+
"description": self.description,
96+
}

opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import logging
2020
import threading
2121
import traceback
22+
import typing
2223
from os import environ
2324
from time import time_ns
2425
from typing import Any, Callable, Optional, Tuple, Union
@@ -37,7 +38,7 @@
3738
OTEL_ATTRIBUTE_COUNT_LIMIT,
3839
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
3940
)
40-
from opentelemetry.sdk.resources import Resource
41+
from opentelemetry.sdk.resources import Resource, ResourceDict
4142
from opentelemetry.sdk.util import ns_to_iso_str
4243
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
4344
from opentelemetry.semconv.trace import SpanAttributes
@@ -147,6 +148,21 @@ def _from_env_if_absent(
147148
)
148149

149150

151+
class LogRecordDict(typing.TypedDict):
152+
"""Dictionary representation of a LogRecord."""
153+
154+
body: typing.Optional[typing.Any]
155+
severity_number: int
156+
severity_text: typing.Optional[str]
157+
attributes: Attributes
158+
dropped_attributes: int
159+
timestamp: typing.Optional[str]
160+
trace_id: str
161+
span_id: str
162+
trace_flags: typing.Optional[int]
163+
resource: typing.Optional[ResourceDict]
164+
165+
150166
class LogRecord(APILogRecord):
151167
"""A LogRecord instance represents an event being logged.
152168
@@ -194,30 +210,28 @@ def __eq__(self, other: object) -> bool:
194210
return NotImplemented
195211
return self.__dict__ == other.__dict__
196212

213+
def to_dict(self) -> LogRecordDict:
214+
return {
215+
"body": self.body,
216+
"severity_number": self.severity_number.value
217+
if self.severity_number is not None
218+
else SeverityNumber.UNSPECIFIED.value,
219+
"severity_text": self.severity_text,
220+
"attributes": dict(self.attributes or {}),
221+
"dropped_attributes": self.dropped_attributes,
222+
"timestamp": ns_to_iso_str(self.timestamp),
223+
"trace_id": f"0x{format_trace_id(self.trace_id)}"
224+
if self.trace_id is not None
225+
else "",
226+
"span_id": f"0x{format_span_id(self.span_id)}"
227+
if self.span_id is not None
228+
else "",
229+
"trace_flags": self.trace_flags,
230+
"resource": self.resource.to_dict() if self.resource else None,
231+
}
232+
197233
def to_json(self, indent=4) -> str:
198-
return json.dumps(
199-
{
200-
"body": self.body,
201-
"severity_number": repr(self.severity_number),
202-
"severity_text": self.severity_text,
203-
"attributes": dict(self.attributes)
204-
if bool(self.attributes)
205-
else None,
206-
"dropped_attributes": self.dropped_attributes,
207-
"timestamp": ns_to_iso_str(self.timestamp),
208-
"trace_id": f"0x{format_trace_id(self.trace_id)}"
209-
if self.trace_id is not None
210-
else "",
211-
"span_id": f"0x{format_span_id(self.span_id)}"
212-
if self.span_id is not None
213-
else "",
214-
"trace_flags": self.trace_flags,
215-
"resource": json.loads(self.resource.to_json())
216-
if self.resource
217-
else None,
218-
},
219-
indent=indent,
220-
)
234+
return json.dumps(self.to_dict(), indent=indent)
221235

222236
@property
223237
def dropped_attributes(self) -> int:

0 commit comments

Comments
 (0)