From ac1d43378c177a99016d0182f73da8df9819e411 Mon Sep 17 00:00:00 2001 From: Tom Monk Date: Wed, 7 Feb 2024 11:22:26 -0800 Subject: [PATCH] SNOW-1023673 Implement common encode and serialize methods using OTEL v0.12.0 --- .github/workflows/build-test.yml | 1 + README.md | 28 +- setup.py | 20 +- src/snowflake/telemetry/__init__.py | 13 +- .../encoder/otlp/proto/common/log_encoder.py | 24 + .../otlp/proto/common/metrics_encoder.py | 25 + .../otlp/proto/common/trace_encoder.py | 29 + src/snowflake/telemetry/version.py | 4 +- .../snowflake-telemetry-test-utils/README.md | 5 + tests/snowflake-telemetry-test-utils/setup.py | 52 ++ .../src/snowflake/telemetry/test/__init__.py | 0 .../telemetry/test/metrictestutil.py | 100 +++ tests/test_log_encoder.py | 302 ++++++++ tests/test_metrics_encoder.py | 698 ++++++++++++++++++ tests/test_trace_encoder.py | 385 ++++++++++ 15 files changed, 1661 insertions(+), 25 deletions(-) create mode 100644 src/snowflake/telemetry/_internal/encoder/otlp/proto/common/log_encoder.py create mode 100644 src/snowflake/telemetry/_internal/encoder/otlp/proto/common/metrics_encoder.py create mode 100644 src/snowflake/telemetry/_internal/encoder/otlp/proto/common/trace_encoder.py create mode 100644 tests/snowflake-telemetry-test-utils/README.md create mode 100644 tests/snowflake-telemetry-test-utils/setup.py create mode 100644 tests/snowflake-telemetry-test-utils/src/snowflake/telemetry/test/__init__.py create mode 100644 tests/snowflake-telemetry-test-utils/src/snowflake/telemetry/test/metrictestutil.py create mode 100644 tests/test_log_encoder.py create mode 100644 tests/test_metrics_encoder.py create mode 100644 tests/test_trace_encoder.py diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 089bee3..4cc16f8 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -43,4 +43,5 @@ jobs: pip install . - name: Test with pytest run: | + pip install ./tests/snowflake-telemetry-test-utils pytest diff --git a/README.md b/README.md index 389fc5f..3cc54e7 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,27 @@ The package is a stub for the full functionality when running in Snowflake. ## Getting started -To install this package, run +To install the latest release of this package as an end user, run ```bash -$ git clone git@github.com:snowflakedb/snowflake-telemetry-python.git -$ cd snowflake-telemetry-python - -$ python3 -m venv venv -$ source venv/bin/activate -$ pip install --upgrade pip -$ pip install . +VERSION="0.2.0" +curl -L "https://github.com/snowflakedb/snowflake-telemetry-python/archive/refs/tags/v${VERSION}.tar.gz" > "snowflake-telemetry-python-${VERSION}.tar.gz" +tar -xvf "snowflake-telemetry-python-${VERSION}.tar.gz" +cd "snowflake-telemetry-python-${VERSION}" +python3 -m venv .venv +source .venv/bin/activate +pip install --upgrade pip +pip install . ``` +To develop this package, run + +```bash +git clone git@github.com:snowflakedb/snowflake-telemetry-python.git +cd snowflake-telemetry-python + +python3 -m venv .venv +source .venv/bin/activate +pip install --upgrade pip +pip install . ./tests/snowflake-telemetry-test-utils +``` diff --git a/setup.py b/setup.py index b90852e..f873605 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,8 @@ import os -from setuptools import setup - +from setuptools import ( + find_namespace_packages, + setup, +) DESCRIPTION = 'Snowflake Telemetry Python' LONG_DESCRIPTION = 'This package provides a set of telemetry APIs for use in Snowflake' @@ -9,26 +11,24 @@ VERSION = None with open(os.path.join(SNOWFLAKE_TELEMETRY_SRC_DIR, "version.py"), encoding="utf-8") as f: exec(f.read()) -VERSION_STR = ".".join([str(v) for v in VERSION if v is not None]) setup( name="snowflake-telemetry-python", - version=VERSION_STR, + version=VERSION, author="Snowflake, Inc", author_email="support@snowflake.com", description=DESCRIPTION, long_description=LONG_DESCRIPTION, install_requires=[ - "setuptools >= 40.0.0", - "opentelemetry-sdk == 1.12.0", + "setuptools >= 40.0.0, < 66.0.0", "opentelemetry-api == 1.12.0", "opentelemetry-exporter-otlp == 1.12.0", + "opentelemetry-sdk == 1.12.0", ], - namespace_packages=["snowflake"], - packages=[ - "snowflake.telemetry", - ], + packages=find_namespace_packages( + where='src' + ), package_dir={ "": "src", }, diff --git a/src/snowflake/telemetry/__init__.py b/src/snowflake/telemetry/__init__.py index 2d66be5..05835fe 100644 --- a/src/snowflake/telemetry/__init__.py +++ b/src/snowflake/telemetry/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2022 Snowflake Computing Inc. All rights reserved. +# Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. # # pylint: disable=unused-argument @@ -11,16 +11,19 @@ Stored Procedures. """ -from opentelemetry.util import types +from opentelemetry.util.types import ( + AttributeValue, + Attributes, +) from snowflake.telemetry.version import VERSION -__version__ = ".".join(str(x) for x in VERSION if x is not None) +__version__ = VERSION def add_event( name: str, - attributes: types.Attributes = None, + attributes: Attributes = None, ) -> None: """Add an event to the Snowflake auto-instrumented span. @@ -28,7 +31,7 @@ def add_event( """ -def set_span_attribute(key: str, value: types.AttributeValue) -> None: +def set_span_attribute(key: str, value: AttributeValue) -> None: """Set an attribute to the Snowflake auto-instrumented span. This is a stub for the full functionality when running in Snowflake. diff --git a/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/log_encoder.py b/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/log_encoder.py new file mode 100644 index 0000000..9c07d12 --- /dev/null +++ b/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/log_encoder.py @@ -0,0 +1,24 @@ +# +# Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. +# + +from logging import LogRecord +from typing import Sequence + +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceRequest +from opentelemetry.proto.logs.v1.logs_pb2 import LogsData + + +def _encode_logs(batch: Sequence[LogRecord]) -> ExportLogsServiceRequest: + # TODO fix this no-op implementation of encode_logs + return ExportLogsServiceRequest(resource_logs=[]) + +def serialize_logs_data(batch: Sequence[LogRecord]) -> bytes: + return LogsData( + resource_logs=_encode_logs(batch).resource_logs + ).SerializeToString() + + +__all__ = [ + "serialize_logs_data", +] diff --git a/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/metrics_encoder.py b/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/metrics_encoder.py new file mode 100644 index 0000000..6a6ccaa --- /dev/null +++ b/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/metrics_encoder.py @@ -0,0 +1,25 @@ +# +# Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. +# + +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceRequest +from opentelemetry.proto.metrics.v1.metrics_pb2 import MetricsData as PB2MetricsData +from opentelemetry.sdk.metrics.export import MetricsData + + +_exporter = OTLPMetricExporter() + +def _encode_metrics(data: MetricsData) -> ExportMetricsServiceRequest: + # Will no longer rely on _translate_data after we upgrade to v1.19.0 or later + return _exporter._translate_data(data) + +def serialize_metrics_data(data: MetricsData) -> bytes: + return PB2MetricsData( + resource_metrics=_encode_metrics(data).resource_metrics + ).SerializeToString() + + +__all__ = [ + "serialize_metrics_data", +] diff --git a/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/trace_encoder.py b/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/trace_encoder.py new file mode 100644 index 0000000..6d70f8d --- /dev/null +++ b/src/snowflake/telemetry/_internal/encoder/otlp/proto/common/trace_encoder.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. +# + +from typing import Sequence + +from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import _ProtobufEncoder +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest +from opentelemetry.proto.trace.v1.trace_pb2 import TracesData +from opentelemetry.sdk.trace import ReadableSpan + + +def _encode_spans( + sdk_spans: Sequence[ReadableSpan], +) -> ExportTraceServiceRequest: + # Will no longer rely on _ProtobufEncoder after we upgrade to v1.19.0 or later + return _ProtobufEncoder.encode(sdk_spans) + +def serialize_traces_data( + sdk_spans: Sequence[ReadableSpan], +) -> bytes: + return TracesData( + resource_spans=_encode_spans(sdk_spans).resource_spans + ).SerializeToString() + + +__all__ = [ + "serialize_traces_data", +] diff --git a/src/snowflake/telemetry/version.py b/src/snowflake/telemetry/version.py index 04f86e7..89f0e58 100644 --- a/src/snowflake/telemetry/version.py +++ b/src/snowflake/telemetry/version.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -# Copyright (c) 2012-2022 Snowflake Computing Inc. All rights reserved. +# Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. # """Update this for the versions.""" -VERSION = (0, 2, 0) +VERSION = "0.2.1.dev" diff --git a/tests/snowflake-telemetry-test-utils/README.md b/tests/snowflake-telemetry-test-utils/README.md new file mode 100644 index 0000000..9a83667 --- /dev/null +++ b/tests/snowflake-telemetry-test-utils/README.md @@ -0,0 +1,5 @@ +# Snowflake Telemetry Test Utils + +## About + +`snowflake-telemetry-test-utils` is a utility module that supports the test written for the Snowflake Telemetry Python project. diff --git a/tests/snowflake-telemetry-test-utils/setup.py b/tests/snowflake-telemetry-test-utils/setup.py new file mode 100644 index 0000000..5c409ca --- /dev/null +++ b/tests/snowflake-telemetry-test-utils/setup.py @@ -0,0 +1,52 @@ +import os +from setuptools import ( + find_namespace_packages, + setup, +) + +DESCRIPTION = 'Snowflake Telemetry Test Utils' +LONG_DESCRIPTION = 'This package provides test utils for testing snowflake-telemetry-python' + + +setup( + name="snowflake-telemetry-test-utils", + version="0.0.1.dev", + author="Snowflake, Inc", + author_email="support@snowflake.com", + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + install_requires=[ + "pytest >= 7.0.0", + "snowflake-telemetry-python == 0.2.1.dev", + ], + packages=find_namespace_packages( + where='src' + ), + package_dir={ + "": "src", + }, + keywords="Snowflake db database cloud analytics warehouse", + classifiers=[ + "Development Status :: 1 - Planning", + "Environment :: Console", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: SQL", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Database", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Information Analysis", + ], + zip_safe=True, +) diff --git a/tests/snowflake-telemetry-test-utils/src/snowflake/telemetry/test/__init__.py b/tests/snowflake-telemetry-test-utils/src/snowflake/telemetry/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/snowflake-telemetry-test-utils/src/snowflake/telemetry/test/metrictestutil.py b/tests/snowflake-telemetry-test-utils/src/snowflake/telemetry/test/metrictestutil.py new file mode 100644 index 0000000..ff25b09 --- /dev/null +++ b/tests/snowflake-telemetry-test-utils/src/snowflake/telemetry/test/metrictestutil.py @@ -0,0 +1,100 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + + +from opentelemetry.attributes import BoundedAttributes +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Gauge, + Metric, + NumberDataPoint, + Sum, +) + + +def _generate_metric( + name, data, attributes=None, description=None, unit=None +) -> Metric: + if description is None: + description = "foo" + if unit is None: + unit = "s" + return Metric( + name=name, + description=description, + unit=unit, + data=data, + ) + + +def _generate_sum( + name, + value, + attributes=None, + description=None, + unit=None, + is_monotonic=True, +) -> Metric: + if attributes is None: + attributes = BoundedAttributes(attributes={"a": 1, "b": True}) + return _generate_metric( + name, + Sum( + data_points=[ + NumberDataPoint( + attributes=attributes, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=value, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=is_monotonic, + ), + description=description, + unit=unit, + ) + + +def _generate_gauge( + name, value, attributes=None, description=None, unit=None +) -> Metric: + if attributes is None: + attributes = BoundedAttributes(attributes={"a": 1, "b": True}) + return _generate_metric( + name, + Gauge( + data_points=[ + NumberDataPoint( + attributes=attributes, + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + value=value, + ) + ], + ), + description=description, + unit=unit, + ) + + +def _generate_unsupported_metric( + name, attributes=None, description=None, unit=None +) -> Metric: + return _generate_metric( + name, + None, + description=description, + unit=unit, + ) diff --git a/tests/test_log_encoder.py b/tests/test_log_encoder.py new file mode 100644 index 0000000..c935f3f --- /dev/null +++ b/tests/test_log_encoder.py @@ -0,0 +1,302 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 unittest +import pytest +from typing import List, Tuple + +from opentelemetry.sdk._logs import SeverityNumber +from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( + _encode_attributes, + _encode_span_id, + _encode_trace_id, + _encode_value, +) +from snowflake.telemetry._internal.encoder.otlp.proto.common.log_encoder import ( + _encode_logs, + serialize_logs_data, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationScope as PB2InstrumentationScope, +) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.proto.logs.v1.logs_pb2 import LogsData as PB2LogsData +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ResourceLogs as PB2ResourceLogs, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.trace import TraceFlags + +@pytest.mark.skip(reason="we are currently re-evaluating the implementation of log_encoder") +class TestOTLPLogEncoder(unittest.TestCase): + def test_encode(self): + sdk_logs, expected_encoding = self.get_test_logs() + self.assertEqual(_encode_logs(sdk_logs), expected_encoding) + + def test_serialize_logs_data(self): + sdk_logs, expected_encoding = self.get_test_logs() + self.assertIsInstance(serialize_logs_data(sdk_logs), bytes) + self.assertEqual(serialize_logs_data(sdk_logs), + PB2LogsData(resource_logs=expected_encoding.resource_logs).SerializeToString()) + + @staticmethod + def _get_sdk_log_data() -> List[LogData]: + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + log3 = LogData( + log_record=SDKLogRecord( + timestamp=1644650427658989056, + trace_id=271615924622795969659406376515024083555, + span_id=4242561578944770265, + trace_flags=TraceFlags(0x01), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body="To our galaxy", + resource=SDKResource({"second_resource": "CASE"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=None, + ) + + log4 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683008, + trace_id=212592107417388365804938480559624925555, + span_id=6077757853989569223, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="Love is the one thing that transcends time and space", + resource=SDKResource({"first_resource": "value"}), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "another_name", "another_version" + ), + ) + + return [log1, log2, log3, log4] + + def get_test_logs( + self, + ) -> Tuple[List[SDKLogRecord], ExportLogsServiceRequest]: + sdk_logs = self._get_sdk_log_data() + + pb2_service_request = ExportLogsServiceRequest( + resource_logs=[ + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="first_resource", + value=PB2AnyValue(string_value="value"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="first_name", version="first_version" + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650195189786880, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 + ), + span_id=_encode_span_id( + 1312458408527513268 + ), + flags=int(TraceFlags(0x01)), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Do not go gentle into that good night. Rage, rage against the dying of the light" + ), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ) + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="another_name", + version="another_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650584292683008, + trace_id=_encode_trace_id( + 212592107417388365804938480559624925555 + ), + span_id=_encode_span_id( + 6077757853989569223 + ), + flags=int(TraceFlags(0x01)), + severity_text="INFO", + severity_number=SeverityNumber.INFO.value, + body=_encode_value( + "Love is the one thing that transcends time and space" + ), + attributes=_encode_attributes( + { + "filename": "model.py", + "func_name": "run_method", + } + ), + ) + ], + ), + ], + ), + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="second_resource", + value=PB2AnyValue(string_value="CASE"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="second_name", + version="second_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650249738562048, + trace_id=_encode_trace_id(0), + span_id=_encode_span_id(0), + flags=int(TraceFlags.DEFAULT), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Cooper, this is no time for caution!" + ), + attributes={}, + ), + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope(), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650427658989056, + trace_id=_encode_trace_id( + 271615924622795969659406376515024083555 + ), + span_id=_encode_span_id( + 4242561578944770265 + ), + flags=int(TraceFlags(0x01)), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG.value, + body=_encode_value("To our galaxy"), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ), + ], + ), + ], + ), + ] + ) + + return sdk_logs, pb2_service_request + + @staticmethod + def _get_test_logs_dropped_attributes() -> List[LogData]: + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c", "user_id": "B121092"} + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + return [log1, log2] diff --git a/tests/test_metrics_encoder.py b/tests/test_metrics_encoder.py new file mode 100644 index 0000000..7e8cbe9 --- /dev/null +++ b/tests/test_metrics_encoder.py @@ -0,0 +1,698 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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=protected-access +import unittest + +from snowflake.telemetry._internal.encoder.otlp.proto.common.metrics_encoder import ( + _encode_metrics, + serialize_metrics_data, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationScope, + KeyValue, +) +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as OTLPResource, +) +from opentelemetry.sdk.metrics.export import AggregationTemporality +from opentelemetry.sdk.metrics.export import Histogram as HistogramType +from opentelemetry.sdk.metrics.export import ( + HistogramDataPoint, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from snowflake.telemetry.test.metrictestutil import _generate_gauge, _generate_sum + + +class TestOTLPMetricsEncoder(unittest.TestCase): + histogram = Metric( + name="histogram", + description="foo", + unit="s", + data=HistogramType( + data_points=[ + HistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + min=8, + max=18, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + + def test_encode_sum_int(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_int", 33)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="sum_int", + unit="s", + description="foo", + sum=pb2.Sum( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + as_int=33, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + ], + ) + ], + ) + ] + ) + actual = _encode_metrics(metrics_data) + self.assertEqual(expected, actual) + self.assertEqual(pb2.MetricsData(resource_metrics=actual.resource_metrics).SerializeToString(), + serialize_metrics_data(metrics_data)) + + def test_encode_sum_double(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_double", 2.98)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="sum_double", + unit="s", + description="foo", + sum=pb2.Sum( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946015139533244, + time_unix_nano=1641946016139533244, + as_double=2.98, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + ], + ) + ], + ) + ] + ) + actual = _encode_metrics(metrics_data) + self.assertEqual(expected, actual) + self.assertEqual(pb2.MetricsData(resource_metrics=actual.resource_metrics).SerializeToString(), + serialize_metrics_data(metrics_data)) + + def test_encode_gauge_int(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_gauge("gauge_int", 9000)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="gauge_int", + unit="s", + description="foo", + gauge=pb2.Gauge( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + time_unix_nano=1641946016139533244, + as_int=9000, + ) + ], + ), + ) + ], + ) + ], + ) + ] + ) + actual = _encode_metrics(metrics_data) + self.assertEqual(expected, actual) + self.assertEqual(pb2.MetricsData(resource_metrics=actual.resource_metrics).SerializeToString(), + serialize_metrics_data(metrics_data)) + + def test_encode_gauge_double(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_gauge("gauge_double", 52.028)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="gauge_double", + unit="s", + description="foo", + gauge=pb2.Gauge( + data_points=[ + pb2.NumberDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + time_unix_nano=1641946016139533244, + as_double=52.028, + ) + ], + ), + ) + ], + ) + ], + ) + ] + ) + actual = _encode_metrics(metrics_data) + self.assertEqual(expected, actual) + self.assertEqual(pb2.MetricsData(resource_metrics=actual.resource_metrics).SerializeToString(), + serialize_metrics_data(metrics_data)) + + def test_encode_histogram(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ) + ], + ) + ] + ) + actual = _encode_metrics(metrics_data) + self.assertEqual(expected, actual) + self.assertEqual(pb2.MetricsData(resource_metrics=actual.resource_metrics).SerializeToString(), + serialize_metrics_data(metrics_data)) + + def test_encode_multiple_scope_histogram(self): + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram, self.histogram], + schema_url="instrumentation_scope_schema_url", + ), + ScopeMetrics( + scope=SDKInstrumentationScope( + name="second_name", + version="second_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram], + schema_url="instrumentation_scope_schema_url", + ), + ScopeMetrics( + scope=SDKInstrumentationScope( + name="third_name", + version="third_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[self.histogram], + schema_url="instrumentation_scope_schema_url", + ), + ], + schema_url="resource_schema_url", + ) + ] + ) + expected = ExportMetricsServiceRequest( + resource_metrics=[ + pb2.ResourceMetrics( + resource=OTLPResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + scope_metrics=[ + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="first_name", version="first_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ), + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ), + ], + ), + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="second_name", version="second_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ), + pb2.ScopeMetrics( + scope=InstrumentationScope( + name="third_name", version="third_version" + ), + metrics=[ + pb2.Metric( + name="histogram", + unit="s", + description="foo", + histogram=pb2.Histogram( + data_points=[ + pb2.HistogramDataPoint( + attributes=[ + KeyValue( + key="a", + value=AnyValue( + int_value=1 + ), + ), + KeyValue( + key="b", + value=AnyValue( + bool_value=True + ), + ), + ], + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + exemplars=[], + max=18.0, + min=8.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + ], + ), + ], + ) + ] + ) + actual = _encode_metrics(metrics_data) + self.assertEqual(expected, actual) + self.assertEqual(pb2.MetricsData(resource_metrics=actual.resource_metrics).SerializeToString(), + serialize_metrics_data(metrics_data)) diff --git a/tests/test_trace_encoder.py b/tests/test_trace_encoder.py new file mode 100644 index 0000000..7ddbbbb --- /dev/null +++ b/tests/test_trace_encoder.py @@ -0,0 +1,385 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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=protected-access + +import unittest +from typing import List, Tuple + +from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( + _SPAN_KIND_MAP, + _encode_span_id, + _encode_status, + _encode_trace_id, +) +from snowflake.telemetry._internal.encoder.otlp.proto.common.trace_encoder import ( + _encode_spans, + serialize_traces_data, +) +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest as PB2ExportTraceServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationScope as PB2InstrumentationScope, +) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ResourceSpans as PB2ResourceSpans, + TracesData as PB2TracesData +) +from opentelemetry.proto.trace.v1.trace_pb2 import ScopeSpans as PB2ScopeSpans +from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan +from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status +from opentelemetry.sdk.trace import Event as SDKEvent +from opentelemetry.sdk.trace import Resource as SDKResource +from opentelemetry.sdk.trace import SpanContext as SDKSpanContext +from opentelemetry.sdk.trace import _Span as SDKSpan +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.trace import Link as SDKLink +from opentelemetry.trace import SpanKind as SDKSpanKind +from opentelemetry.trace import TraceFlags as SDKTraceFlags +from opentelemetry.trace.status import Status as SDKStatus +from opentelemetry.trace.status import StatusCode as SDKStatusCode + + +class TestOTLPTraceEncoder(unittest.TestCase): + def test_encode_spans(self): + otel_spans, expected_encoding = self.get_exhaustive_test_spans() + self.assertEqual(_encode_spans(otel_spans), expected_encoding) + + def test_serialize_traces_data(self): + otel_spans, expected_encoding = self.get_exhaustive_test_spans() + self.assertEqual(serialize_traces_data(otel_spans), + PB2TracesData(resource_spans=expected_encoding.resource_spans).SerializeToString()) + + @staticmethod + def get_exhaustive_otel_span_list() -> List[SDKSpan]: + trace_id = 0x3E0C63257DE34C926F9EFCD03927272E + + base_time = 683647322 * 10**9 # in ns + start_times = ( + base_time, + base_time + 150 * 10**6, + base_time + 300 * 10**6, + base_time + 400 * 10**6, + ) + end_times = ( + start_times[0] + (50 * 10**6), + start_times[1] + (100 * 10**6), + start_times[2] + (200 * 10**6), + start_times[3] + (300 * 10**6), + ) + + parent_span_context = SDKSpanContext( + trace_id, 0x1111111111111111, is_remote=False + ) + + other_context = SDKSpanContext( + trace_id, 0x2222222222222222, is_remote=False + ) + + span1 = SDKSpan( + name="test-span-1", + context=SDKSpanContext( + trace_id, + 0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), + ), + parent=parent_span_context, + events=( + SDKEvent( + name="event0", + timestamp=base_time + 50 * 10**6, + attributes={ + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + }, + ), + ), + links=( + SDKLink(context=other_context, attributes={"key_bool": True}), + ), + resource=SDKResource({}), + ) + span1.start(start_time=start_times[0]) + span1.set_attribute("key_bool", False) + span1.set_attribute("key_string", "hello_world") + span1.set_attribute("key_float", 111.22) + span1.set_status(SDKStatus(SDKStatusCode.ERROR, "Example description")) + span1.end(end_time=end_times[0]) + + span2 = SDKSpan( + name="test-span-2", + context=parent_span_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span2.start(start_time=start_times[1]) + span2.end(end_time=end_times[1]) + + span3 = SDKSpan( + name="test-span-3", + context=other_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span3.start(start_time=start_times[2]) + span3.set_attribute("key_string", "hello_world") + span3.end(end_time=end_times[2]) + + span4 = SDKSpan( + name="test-span-4", + context=other_context, + parent=None, + resource=SDKResource({}), + instrumentation_scope=SDKInstrumentationScope( + name="name", version="version" + ), + ) + span4.start(start_time=start_times[3]) + span4.end(end_time=end_times[3]) + + return [span1, span2, span3, span4] + + def get_exhaustive_test_spans( + self, + ) -> Tuple[List[SDKSpan], PB2ExportTraceServiceRequest]: + otel_spans = self.get_exhaustive_otel_span_list() + trace_id = _encode_trace_id(otel_spans[0].context.trace_id) + span_kind = _SPAN_KIND_MAP[SDKSpanKind.INTERNAL] + + pb2_service_request = PB2ExportTraceServiceRequest( + resource_spans=[ + PB2ResourceSpans( + resource=PB2Resource(), + scope_spans=[ + PB2ScopeSpans( + scope=PB2InstrumentationScope(), + spans=[ + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[0].context.span_id + ), + trace_state=None, + parent_span_id=_encode_span_id( + otel_spans[0].parent.span_id + ), + name=otel_spans[0].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 0 + ].start_time, + end_time_unix_nano=otel_spans[0].end_time, + attributes=[ + PB2KeyValue( + key="key_bool", + value=PB2AnyValue( + bool_value=False + ), + ), + PB2KeyValue( + key="key_string", + value=PB2AnyValue( + string_value="hello_world" + ), + ), + PB2KeyValue( + key="key_float", + value=PB2AnyValue( + double_value=111.22 + ), + ), + ], + events=[ + PB2SPan.Event( + name="event0", + time_unix_nano=otel_spans[0] + .events[0] + .timestamp, + attributes=[ + PB2KeyValue( + key="annotation_bool", + value=PB2AnyValue( + bool_value=True + ), + ), + PB2KeyValue( + key="annotation_string", + value=PB2AnyValue( + string_value="annotation_test" + ), + ), + PB2KeyValue( + key="key_float", + value=PB2AnyValue( + double_value=0.3 + ), + ), + ], + ) + ], + links=[ + PB2SPan.Link( + trace_id=_encode_trace_id( + otel_spans[0] + .links[0] + .context.trace_id + ), + span_id=_encode_span_id( + otel_spans[0] + .links[0] + .context.span_id + ), + attributes=[ + PB2KeyValue( + key="key_bool", + value=PB2AnyValue( + bool_value=True + ), + ), + ], + ) + ], + status=PB2Status( + code=SDKStatusCode.ERROR.value, + message="Example description", + ), + ) + ], + ), + PB2ScopeSpans( + scope=PB2InstrumentationScope( + name="name", + version="version", + ), + spans=[ + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[3].context.span_id + ), + trace_state=None, + parent_span_id=None, + name=otel_spans[3].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 3 + ].start_time, + end_time_unix_nano=otel_spans[3].end_time, + attributes=None, + events=None, + links=None, + status={}, + ) + ], + ), + ], + ), + PB2ResourceSpans( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="key_resource", + value=PB2AnyValue( + string_value="some_resource" + ), + ) + ] + ), + scope_spans=[ + PB2ScopeSpans( + scope=PB2InstrumentationScope(), + spans=[ + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[1].context.span_id + ), + trace_state=None, + parent_span_id=None, + name=otel_spans[1].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 1 + ].start_time, + end_time_unix_nano=otel_spans[1].end_time, + attributes=None, + events=None, + links=None, + status={}, + ), + PB2SPan( + trace_id=trace_id, + span_id=_encode_span_id( + otel_spans[2].context.span_id + ), + trace_state=None, + parent_span_id=None, + name=otel_spans[2].name, + kind=span_kind, + start_time_unix_nano=otel_spans[ + 2 + ].start_time, + end_time_unix_nano=otel_spans[2].end_time, + attributes=[ + PB2KeyValue( + key="key_string", + value=PB2AnyValue( + string_value="hello_world" + ), + ), + ], + events=None, + links=None, + status={}, + ), + ], + ) + ], + ), + ] + ) + + return otel_spans, pb2_service_request + + def test_encode_status_code_translations(self): + self.assertEqual( + _encode_status(SDKStatus(status_code=SDKStatusCode.UNSET)), + PB2Status( + code=SDKStatusCode.UNSET.value, + ), + ) + + self.assertEqual( + _encode_status(SDKStatus(status_code=SDKStatusCode.OK)), + PB2Status( + code=SDKStatusCode.OK.value, + ), + ) + + self.assertEqual( + _encode_status(SDKStatus(status_code=SDKStatusCode.ERROR)), + PB2Status( + code=SDKStatusCode.ERROR.value, + ), + )