Skip to content

Commit

Permalink
Introduce to_dict
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sernst committed Jul 2, 2023
1 parent 37de27a commit cb1a3ce
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 207 deletions.
14 changes: 14 additions & 0 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
NonRecordingSpan,
Span,
SpanContext,
SpanContextDict,
TraceFlags,
TraceState,
format_span_id,
Expand Down Expand Up @@ -130,6 +131,13 @@ def attributes(self) -> types.Attributes:
pass


class LinkDict(typing.TypedDict):
"""Dictionary representation of a span Link."""

context: SpanContextDict
attributes: types.Attributes


class Link(_LinkBase):
"""A link to a `Span`. The attributes of a Link are immutable.
Expand All @@ -152,6 +160,12 @@ def __init__(
def attributes(self) -> types.Attributes:
return self._attributes

def to_dict(self) -> LinkDict:
return {
"context": self.context.to_dict(),
"attributes": dict(self._attributes),
}


_Links = Optional[Sequence[Link]]

Expand Down
15 changes: 15 additions & 0 deletions opentelemetry-api/src/opentelemetry/trace/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,14 @@ def values(self) -> typing.ValuesView[str]:
_SPAN_ID_MAX_VALUE = 2**64 - 1


class SpanContextDict(typing.TypedDict):
"""Dictionary representation of a SpanContext."""

trace_id: str
span_id: str
trace_state: typing.Dict[str, str]


class SpanContext(
typing.Tuple[int, int, bool, "TraceFlags", "TraceState", bool]
):
Expand Down Expand Up @@ -477,6 +485,13 @@ def trace_state(self) -> "TraceState":
def is_valid(self) -> bool:
return self[5] # pylint: disable=unsubscriptable-object

def to_dict(self) -> SpanContextDict:
return {
"trace_id": f"0x{format_trace_id(self.trace_id)}",
"span_id": f"0x{format_span_id(self.span_id)}",
"trace_state": dict(self.trace_state),
}

def __setattr__(self, *args: str) -> None:
_logger.debug(
"Immutable type, ignoring call to set attribute", stack_info=True
Expand Down
14 changes: 14 additions & 0 deletions opentelemetry-api/src/opentelemetry/trace/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ class StatusCode(enum.Enum):
"""The operation contains an error."""


class StatusDict(typing.TypedDict):
"""Dictionary representation of a trace Status."""

status_code: str
description: typing.Optional[str]


class Status:
"""Represents the status of a finished Span.
Expand Down Expand Up @@ -80,3 +87,10 @@ def is_ok(self) -> bool:
def is_unset(self) -> bool:
"""Returns true if unset, false otherwise."""
return self._status_code is StatusCode.UNSET

def to_dict(self) -> StatusDict:
"""Convert to a dictionary representation of the status."""
return {
"status_code": str(self.status_code.name),
"description": self.description,
}
62 changes: 38 additions & 24 deletions opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import logging
import threading
import traceback
import typing
from os import environ
from time import time_ns
from typing import Any, Callable, Optional, Tuple, Union
Expand All @@ -37,7 +38,7 @@
OTEL_ATTRIBUTE_COUNT_LIMIT,
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.resources import Resource, ResourceDict
from opentelemetry.sdk.util import ns_to_iso_str
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.semconv.trace import SpanAttributes
Expand Down Expand Up @@ -147,6 +148,21 @@ def _from_env_if_absent(
)


class LogRecordDict(typing.TypedDict):
"""Dictionary representation of a LogRecord."""

body: typing.Optional[typing.Any]
severity_number: int
severity_text: typing.Optional[str]
attributes: Attributes
dropped_attributes: int
timestamp: typing.Optional[str]
trace_id: str
span_id: str
trace_flags: typing.Optional[int]
resource: typing.Optional[ResourceDict]


class LogRecord(APILogRecord):
"""A LogRecord instance represents an event being logged.
Expand Down Expand Up @@ -194,30 +210,28 @@ def __eq__(self, other: object) -> bool:
return NotImplemented
return self.__dict__ == other.__dict__

def to_dict(self) -> LogRecordDict:
return {
"body": self.body,
"severity_number": self.severity_number.value
if self.severity_number is not None
else SeverityNumber.UNSPECIFIED.value,
"severity_text": self.severity_text,
"attributes": dict(self.attributes or {}),
"dropped_attributes": self.dropped_attributes,
"timestamp": ns_to_iso_str(self.timestamp),
"trace_id": f"0x{format_trace_id(self.trace_id)}"
if self.trace_id is not None
else "",
"span_id": f"0x{format_span_id(self.span_id)}"
if self.span_id is not None
else "",
"trace_flags": self.trace_flags,
"resource": self.resource.to_dict() if self.resource else None,
}

def to_json(self, indent=4) -> str:
return json.dumps(
{
"body": self.body,
"severity_number": repr(self.severity_number),
"severity_text": self.severity_text,
"attributes": dict(self.attributes)
if bool(self.attributes)
else None,
"dropped_attributes": self.dropped_attributes,
"timestamp": ns_to_iso_str(self.timestamp),
"trace_id": f"0x{format_trace_id(self.trace_id)}"
if self.trace_id is not None
else "",
"span_id": f"0x{format_span_id(self.span_id)}"
if self.span_id is not None
else "",
"trace_flags": self.trace_flags,
"resource": json.loads(self.resource.to_json())
if self.resource
else None,
},
indent=indent,
)
return json.dumps(self.to_dict(), indent=indent)

@property
def dropped_attributes(self) -> int:
Expand Down
Loading

0 comments on commit cb1a3ce

Please sign in to comment.