diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ecca6af2e6..0da81ffae98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index bf9e0b89a45..dcf6bf12769 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -103,6 +103,7 @@ NonRecordingSpan, Span, SpanContext, + SpanContextDict, TraceFlags, TraceState, format_span_id, @@ -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. @@ -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]] diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 805b2b06b18..cfc88274926 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -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] ): @@ -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 diff --git a/opentelemetry-api/src/opentelemetry/trace/status.py b/opentelemetry-api/src/opentelemetry/trace/status.py index ada7fa1ebda..067a1366c3c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/status.py +++ b/opentelemetry-api/src/opentelemetry/trace/status.py @@ -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. @@ -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, + } diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index cfa4d6cfa9b..0745282d1fa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -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 @@ -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 @@ -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. @@ -195,30 +211,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": repr(self.resource.attributes) - if self.resource - else "", - }, - indent=indent, - ) + return json.dumps(self.to_dict(), indent=indent) @property def dropped_attributes(self) -> int: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index c30705c59a4..722f495170f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -11,20 +11,28 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - # pylint: disable=unused-import - -from dataclasses import asdict, dataclass -from json import dumps, loads +import typing +from dataclasses import dataclass +from json import dumps from typing import Optional, Sequence, Union # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal -from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.resources import Resource, ResourceDict from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes +class NumberDataPointDict(typing.TypedDict): + """Dictionary representation of a NumberDataPoint.""" + + attributes: Attributes + start_time_unix_nano: int + time_unix_nano: int + value: Union[int, float] + + @dataclass(frozen=True) class NumberDataPoint: """Single data point in a timeseries that describes the time-varying scalar @@ -36,8 +44,30 @@ class NumberDataPoint: time_unix_nano: int value: Union[int, float] + def to_dict(self) -> NumberDataPointDict: + return { + "attributes": dict(self.attributes), + "start_time_unix_nano": self.start_time_unix_nano, + "time_unix_nano": self.time_unix_nano, + "value": self.value, + } + def to_json(self, indent=4) -> str: - return dumps(asdict(self), indent=indent) + return dumps(self.to_dict(), indent=indent) + + +class HistogramDataPointDict(typing.TypedDict): + """Dictionary representation of a HistogramDataPoint.""" + + attributes: Attributes + start_time_unix_nano: int + time_unix_nano: int + count: int + sum: Union[int, float] + bucket_counts: typing.List[int] + explicit_bounds: typing.List[float] + min: float + max: float @dataclass(frozen=True) @@ -56,8 +86,28 @@ class HistogramDataPoint: min: float max: float + def to_dict(self) -> HistogramDataPointDict: + return { + "attributes": dict(self.attributes), + "start_time_unix_nano": self.start_time_unix_nano, + "time_unix_nano": self.time_unix_nano, + "count": self.count, + "sum": self.sum, + "bucket_counts": list(self.bucket_counts), + "explicit_bounds": list(self.explicit_bounds), + "min": self.min, + "max": self.max, + } + def to_json(self, indent=4) -> str: - return dumps(asdict(self), indent=indent) + return dumps(self.to_dict(), indent=indent) + + +class BucketsDict(typing.TypedDict): + """Dictionary representation of a Buckets object.""" + + offset: int + bucket_counts: typing.List[int] @dataclass(frozen=True) @@ -65,6 +115,29 @@ class Buckets: offset: int bucket_counts: Sequence[int] + def to_dict(self) -> BucketsDict: + return { + "offset": self.offset, + "bucket_counts": list(self.bucket_counts), + } + + +class ExponentialHistogramDataPointDict(typing.TypedDict): + """Dictionary representation of an ExponentialHistogramDataPoint.""" + + attributes: Attributes + start_time_unix_nano: int + time_unix_nano: int + count: int + sum: Union[int, float] + scale: int + zero_count: int + positive: BucketsDict + negative: BucketsDict + flags: int + min: float + max: float + @dataclass(frozen=True) class ExponentialHistogramDataPoint: @@ -86,8 +159,31 @@ class ExponentialHistogramDataPoint: min: float max: float + def to_dict(self) -> ExponentialHistogramDataPointDict: + return { + "attributes": dict(self.attributes), + "start_time_unix_nano": self.start_time_unix_nano, + "time_unix_nano": self.time_unix_nano, + "count": self.count, + "sum": self.sum, + "scale": self.scale, + "zero_count": self.zero_count, + "positive": self.positive.to_dict(), + "negative": self.negative.to_dict(), + "flags": self.flags, + "min": self.min, + "max": self.max, + } + def to_json(self, indent=4) -> str: - return dumps(asdict(self), indent=indent) + return dumps(self.to_dict(), indent=indent) + + +class ExponentialHistogramDict(typing.TypedDict): + """Dictionary representation of an ExponentialHistogram.""" + + data_points: typing.List[ExponentialHistogramDataPointDict] + aggregation_temporality: int @dataclass(frozen=True) @@ -101,6 +197,22 @@ class ExponentialHistogram: "opentelemetry.sdk.metrics.export.AggregationTemporality" ) + def to_dict(self) -> ExponentialHistogramDict: + return { + "data_points": [ + data_point.to_dict() for data_point in self.data_points + ], + "aggregation_temporality": self.aggregation_temporality.value, + } + + +class SumDict(typing.TypedDict): + """Dictionary representation of a Sum.""" + + data_points: typing.List[NumberDataPointDict] + aggregation_temporality: int + is_monotonic: bool + @dataclass(frozen=True) class Sum: @@ -113,18 +225,23 @@ class Sum: ) is_monotonic: bool + def to_dict(self) -> SumDict: + return { + "data_points": [ + data_point.to_dict() for data_point in self.data_points + ], + "aggregation_temporality": self.aggregation_temporality.value, + "is_monotonic": self.is_monotonic, + } + def to_json(self, indent=4) -> str: - return dumps( - { - "data_points": [ - loads(data_point.to_json(indent=indent)) - for data_point in self.data_points - ], - "aggregation_temporality": self.aggregation_temporality, - "is_monotonic": self.is_monotonic, - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) + + +class GaugeDict(typing.TypedDict): + """Dictionary representation of a Gauge.""" + + data_points: typing.List[NumberDataPointDict] @dataclass(frozen=True) @@ -135,16 +252,22 @@ class Gauge: data_points: Sequence[NumberDataPoint] + def to_dict(self) -> GaugeDict: + return { + "data_points": [ + data_point.to_dict() for data_point in self.data_points + ], + } + def to_json(self, indent=4) -> str: - return dumps( - { - "data_points": [ - loads(data_point.to_json(indent=indent)) - for data_point in self.data_points - ], - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) + + +class HistogramDict(typing.TypedDict): + """Dictionary representation of a Histogram.""" + + data_points: typing.List[HistogramDataPointDict] + aggregation_temporality: int @dataclass(frozen=True) @@ -157,22 +280,34 @@ class Histogram: "opentelemetry.sdk.metrics.export.AggregationTemporality" ) + def to_dict(self) -> HistogramDict: + return { + "data_points": [ + data_point.to_dict() for data_point in self.data_points + ], + "aggregation_temporality": self.aggregation_temporality.value, + } + def to_json(self, indent=4) -> str: - return dumps( - { - "data_points": [ - loads(data_point.to_json(indent=indent)) - for data_point in self.data_points - ], - "aggregation_temporality": self.aggregation_temporality, - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) # pylint: disable=invalid-name -DataT = Union[Sum, Gauge, Histogram] -DataPointT = Union[NumberDataPoint, HistogramDataPoint] +DataT = Union[Sum, Gauge, Histogram, ExponentialHistogram] +DataPointT = Union[ + NumberDataPoint, HistogramDataPoint, ExponentialHistogramDataPoint +] + + +class MetricDict(typing.TypedDict): + """Dictionary representation of a Metric.""" + + name: str + description: str + unit: str + data: typing.Union[ + SumDict, GaugeDict, HistogramDict, ExponentialHistogramDict + ] @dataclass(frozen=True) @@ -185,16 +320,24 @@ class Metric: unit: Optional[str] data: DataT + def to_dict(self) -> MetricDict: + return { + "name": self.name, + "description": self.description or "", + "unit": self.unit or "", + "data": self.data.to_dict(), + } + def to_json(self, indent=4) -> str: - return dumps( - { - "name": self.name, - "description": self.description or "", - "unit": self.unit or "", - "data": loads(self.data.to_json(indent=indent)), - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) + + +class ScopeMetricsDict(typing.TypedDict): + """Dictionary representation of a ScopeMetrics object.""" + + scope: typing.Any + metrics: typing.List[typing.Any] + schema_url: str @dataclass(frozen=True) @@ -205,18 +348,23 @@ class ScopeMetrics: metrics: Sequence[Metric] schema_url: str + def to_dict(self) -> ScopeMetricsDict: + return { + "scope": self.scope.to_dict(), + "metrics": [metric.to_dict() for metric in self.metrics], + "schema_url": self.schema_url, + } + def to_json(self, indent=4) -> str: - return dumps( - { - "scope": loads(self.scope.to_json(indent=indent)), - "metrics": [ - loads(metric.to_json(indent=indent)) - for metric in self.metrics - ], - "schema_url": self.schema_url, - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) + + +class ResourceMetricsDict(typing.TypedDict): + """Dictionary representation of a ResourceMetrics object.""" + + resource: ResourceDict + scope_metrics: typing.List[ScopeMetricsDict] + schema_url: str @dataclass(frozen=True) @@ -227,18 +375,23 @@ class ResourceMetrics: scope_metrics: Sequence[ScopeMetrics] schema_url: str + def to_dict(self) -> ResourceMetricsDict: + return { + "resource": self.resource.to_dict(), + "scope_metrics": [ + scope_metrics.to_dict() for scope_metrics in self.scope_metrics + ], + "schema_url": self.schema_url, + } + def to_json(self, indent=4) -> str: - return dumps( - { - "resource": loads(self.resource.to_json(indent=indent)), - "scope_metrics": [ - loads(scope_metrics.to_json(indent=indent)) - for scope_metrics in self.scope_metrics - ], - "schema_url": self.schema_url, - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) + + +class MetricsDataDict(typing.TypedDict): + """Dictionary representation of a MetricsData object.""" + + resource_metrics: typing.List[ResourceMetricsDict] @dataclass(frozen=True) @@ -247,13 +400,13 @@ class MetricsData: resource_metrics: Sequence[ResourceMetrics] + def to_dict(self) -> MetricsDataDict: + return { + "resource_metrics": [ + resource_metrics.to_dict() + for resource_metrics in self.resource_metrics + ] + } + def to_json(self, indent=4) -> str: - return dumps( - { - "resource_metrics": [ - loads(resource_metrics.to_json(indent=indent)) - for resource_metrics in self.resource_metrics - ] - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 852b23f5002..8dee4d8cddc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -144,6 +144,13 @@ _OPENTELEMETRY_SDK_VERSION = version("opentelemetry-sdk") +class ResourceDict(typing.TypedDict): + """Dictionary representation of a Resource.""" + + attributes: Attributes + schema_url: typing.Optional[str] + + class Resource: """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" @@ -273,14 +280,14 @@ def __hash__(self): f"{dumps(self._attributes.copy(), sort_keys=True)}|{self._schema_url}" ) + def to_dict(self) -> ResourceDict: + return { + "attributes": dict(self._attributes), + "schema_url": self._schema_url, + } + def to_json(self, indent=4) -> str: - return dumps( - { - "attributes": dict(self._attributes), - "schema_url": self._schema_url, - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) _EMPTY_RESOURCE = Resource({}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 6dae70b2f6b..a0e2f3b89f5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -55,7 +55,7 @@ OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, ) -from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.resources import Resource, ResourceDict from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.util import BoundedList @@ -63,8 +63,8 @@ InstrumentationInfo, InstrumentationScope, ) -from opentelemetry.trace import SpanContext -from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.trace import SpanContext, SpanContextDict +from opentelemetry.trace.status import Status, StatusCode, StatusDict from opentelemetry.util import types logger = logging.getLogger(__name__) @@ -304,6 +304,14 @@ def attributes(self) -> types.Attributes: pass +class EventDict(typing.TypedDict): + """Dictionary representation of a span Event.""" + + name: str + timestamp: str + attributes: types.Attributes + + class Event(EventBase): """A text annotation with a set of attributes. The attributes of an event are immutable. @@ -329,6 +337,13 @@ def __init__( def attributes(self) -> types.Attributes: return self._attributes + def to_dict(self) -> EventDict: + return { + "name": self.name, + "timestamp": util.ns_to_iso_str(self.timestamp), + "attributes": dict(self._attributes), + } + def _check_span_ended(func): def wrapper(self, *args, **kwargs): @@ -345,6 +360,22 @@ def wrapper(self, *args, **kwargs): return wrapper +class ReadableSpanDict(typing.TypedDict): + """Dictionary representation of a ReadableSpan.""" + + name: typing.Optional[str] + context: SpanContextDict + kind: str + parent_id: typing.Optional[str] + start_time: typing.Optional[str] + end_time: typing.Optional[str] + status: typing.Optional[StatusDict] + attributes: types.Attributes + events: typing.List[EventDict] + links: typing.List[trace_api.LinkDict] + resource: ResourceDict + + class ReadableSpan: """Provides read-only access to span attributes. @@ -462,26 +493,27 @@ def instrumentation_info(self) -> Optional[InstrumentationInfo]: def instrumentation_scope(self) -> Optional[InstrumentationScope]: return self._instrumentation_scope - def to_json(self, indent: int = 4): - parent_id = None + def to_json(self, indent=4): + return json.dumps(self.to_dict(), indent=indent) + + def to_dict(self) -> ReadableSpanDict: + parent_id: typing.Optional[str] = None if self.parent is not None: parent_id = f"0x{trace_api.format_span_id(self.parent.span_id)}" - start_time = None + start_time: typing.Optional[str] = None if self._start_time: start_time = util.ns_to_iso_str(self._start_time) - end_time = None + end_time: typing.Optional[str] = None if self._end_time: end_time = util.ns_to_iso_str(self._end_time) - status = { - "status_code": str(self._status.status_code.name), - } - if self._status.description: - status["description"] = self._status.description + status: typing.Optional[StatusDict] = None + if self._status is not None: + status = self.status.to_dict() - f_span = { + return { "name": self._name, "context": self._format_context(self._context) if self._context @@ -491,14 +523,12 @@ def to_json(self, indent: int = 4): "start_time": start_time, "end_time": end_time, "status": status, - "attributes": self._format_attributes(self._attributes), - "events": self._format_events(self._events), - "links": self._format_links(self._links), - "resource": json.loads(self.resource.to_json()), + "attributes": dict(self._attributes), + "events": [event.to_dict() for event in self._events], + "links": [link.to_dict() for link in self._links], + "resource": self.resource.to_dict(), } - return json.dumps(f_span, indent=indent) - @staticmethod def _format_context(context: SpanContext) -> Dict[str, str]: return { diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index 085d3fd874f..96a45588217 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import typing from json import dumps from typing import Optional @@ -74,6 +75,14 @@ def name(self) -> str: return self._name +class InstrumentationScopeDict(typing.TypedDict): + """Dictionary representation of an InstrumentationScope.""" + + name: str + version: typing.Optional[str] + schema_url: typing.Optional[str] + + class InstrumentationScope: """A logical unit of the application code with which the emitted telemetry can be associated. @@ -132,12 +141,12 @@ def version(self) -> Optional[str]: def name(self) -> str: return self._name + def to_dict(self) -> InstrumentationScopeDict: + return { + "name": self._name, + "version": self._version, + "schema_url": self._schema_url, + } + def to_json(self, indent=4) -> str: - return dumps( - { - "name": self._name, - "version": self._version, - "schema_url": self._schema_url, - }, - indent=indent, - ) + return dumps(self.to_dict(), indent=indent) diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 1f0bd785a85..4d45cc3a968 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -17,6 +17,7 @@ from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk._logs import LogLimits, LogRecord +from opentelemetry.sdk.resources import Resource class TestLogRecord(unittest.TestCase): @@ -24,15 +25,15 @@ def test_log_record_to_json(self): expected = json.dumps( { "body": "a log line", - "severity_number": "None", + "severity_number": 0, "severity_text": None, - "attributes": None, + "attributes": {}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "trace_id": "", "span_id": "", "trace_flags": None, - "resource": "", + "resource": None, }, indent=4, ) @@ -42,6 +43,31 @@ def test_log_record_to_json(self): ).to_json() self.assertEqual(expected, actual) + def test_log_record_to_json_with_resource(self): + """Should JSON serialize/deserialize Resource objects within log records.""" + expected = json.dumps( + { + "body": "a log line", + "severity_number": 0, + "severity_text": None, + "attributes": {}, + "dropped_attributes": 0, + "timestamp": "1970-01-01T00:00:00.000000Z", + "trace_id": "", + "span_id": "", + "trace_flags": None, + "resource": {"attributes": {"foo": "bar"}, "schema_url": ""}, + }, + indent=4, + ) + + actual = LogRecord( + timestamp=0, + body="a log line", + resource=Resource(attributes={"foo": "bar"}), + ).to_json() + self.assertEqual(expected, actual) + def test_log_record_bounded_attributes(self): attr = {"key": "value"} diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 4150d60d104..b44220173ff 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -1376,10 +1376,15 @@ def test_to_json(self): is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - parent = trace._Span("parent-name", context, resource=Resource({})) + parent = trace._Span( + "parent-name", + context, + resource=Resource({"hello": "world"}), + ) span = trace._Span( "span-name", context, resource=Resource({}), parent=parent.context ) + span.add_event("foo", {"spam": "ham"}, 1234567890987654321) self.assertEqual( span.to_json(), @@ -1388,17 +1393,26 @@ def test_to_json(self): "context": { "trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", - "trace_state": "[]" + "trace_state": {} }, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": { - "status_code": "UNSET" + "status_code": "UNSET", + "description": null }, "attributes": {}, - "events": [], + "events": [ + { + "name": "foo", + "timestamp": "2009-02-13T23:31:30.987654Z", + "attributes": { + "spam": "ham" + } + } + ], "links": [], "resource": { "attributes": {}, @@ -1408,7 +1422,7 @@ def test_to_json(self): ) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {"attributes": {}, "schema_url": ""}}', + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": {}}, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": {"status_code": "UNSET", "description": null}, "attributes": {}, "events": [{"name": "foo", "timestamp": "2009-02-13T23:31:30.987654Z", "attributes": {"spam": "ham"}}], "links": [], "resource": {"attributes": {}, "schema_url": ""}}', ) def test_attributes_to_json(self): @@ -1424,7 +1438,7 @@ def test_attributes_to_json(self): date_str = ns_to_iso_str(123) self.assertEqual( span.to_json(indent=None), - '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": {}}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET", "description": null}, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + date_str + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {"attributes": {}, "schema_url": ""}}', )