Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- LogRecord now JSON serializes resource objects to match ReadableSpan and MetricsData equivalents
([#3365](https://github.com/open-telemetry/opentelemetry-python/issues/3365))
- Include key in attribute sequence warning
([#3639](https://github.com/open-telemetry/opentelemetry-python/pull/3639))
- Upgrade markupsafe, Flask and related dependencies to dev and test
Expand Down
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typing.TypedDict was introduced in 3.8. This is why tests are failing for pypy and 3.7.

"""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,
}
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 # noqa
Expand All @@ -38,7 +39,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 @@ -148,6 +149,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 @@ -195,30 +211,28 @@ def __eq__(self, other: object) -> bool:
return NotImplemented
return self.__dict__ == other.__dict__

def to_dict(self) -> LogRecordDict:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not add a new method here, but use __iter__ and dict instead:

# dict_test.py
class LogRecord:

    def __init__(self, body, severity_number):

        self.body = body
        self.severity_number = severity_number

    def __iter__(self):

        for key in self.__dict__:
            yield key, getattr(self, key)


log_record = LogRecord("the body", "1")

print(dict(log_record))
tigre@hilleman:~/sandbox$ python3 dict_test.py 
{'body': 'the body', 'severity_number': '1'}

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": repr(self.resource.attributes)
if self.resource
else "",
},
indent=indent,
)
return json.dumps(self.to_dict(), indent=indent)

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