Skip to content

Commit b48b66e

Browse files
sernstocelotl
authored andcommitted
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 d276002 commit b48b66e

File tree

10 files changed

+419
-148
lines changed

10 files changed

+419
-148
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 # noqa
@@ -38,7 +39,7 @@
3839
OTEL_ATTRIBUTE_COUNT_LIMIT,
3940
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
4041
)
41-
from opentelemetry.sdk.resources import Resource
42+
from opentelemetry.sdk.resources import Resource, ResourceDict
4243
from opentelemetry.sdk.util import ns_to_iso_str
4344
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
4445
from opentelemetry.semconv.trace import SpanAttributes
@@ -148,6 +149,21 @@ def _from_env_if_absent(
148149
)
149150

150151

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

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

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

0 commit comments

Comments
 (0)