Skip to content

Commit

Permalink
Merge pull request #470 from solarwinds/NH-97751-apm-proto-addinfo-sp…
Browse files Browse the repository at this point in the history
…an-status

NH-97751 otel.status_code, otel.status_description at APM-proto span export
  • Loading branch information
tammy-baylis-swi authored Jan 2, 2025
2 parents ca70c46 + c8bedfc commit 8676416
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 3 deletions.
2 changes: 2 additions & 0 deletions solarwinds_apm/apm_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
INTL_SWO_LIBOBOE_TXN_NAME_KEY_PREFIX = "oboe"
INTL_SWO_OTEL_SCOPE_NAME = "otel.scope.name"
INTL_SWO_OTEL_SCOPE_VERSION = "otel.scope.version"
INTL_SWO_OTEL_STATUS_CODE = "otel.status_code"
INTL_SWO_OTEL_STATUS_DESCRIPTION = "otel.status_description"
INTL_SWO_TRACESTATE_KEY = "sw"
INTL_SWO_TRANSACTION_ATTR_KEY = "sw.transaction"
INTL_SWO_TRANSACTION_ATTR_MAX = 255
Expand Down
22 changes: 21 additions & 1 deletion solarwinds_apm/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
from typing import Any

from opentelemetry.sdk.trace.export import SpanExporter
from opentelemetry.trace import SpanKind
from opentelemetry.trace import SpanKind, StatusCode
from opentelemetry.util._importlib_metadata import version

from solarwinds_apm.apm_constants import (
INTL_SWO_LIBOBOE_TXN_NAME_KEY_PREFIX,
INTL_SWO_OTEL_SCOPE_NAME,
INTL_SWO_OTEL_SCOPE_VERSION,
INTL_SWO_OTEL_STATUS_CODE,
INTL_SWO_OTEL_STATUS_DESCRIPTION,
INTL_SWO_SUPPORT_EMAIL,
)
from solarwinds_apm.w3c_transformer import W3CTransformer
Expand Down Expand Up @@ -102,6 +104,7 @@ def export(self, spans) -> None:
evt.addInfo(self._SW_SPAN_KIND, span.kind.name)
evt.addInfo("Language", "Python")
self._add_info_instrumentation_scope(span, evt)
self._add_info_status(span, evt)
self._add_info_instrumented_framework(span, evt)
for attr_k, attr_v in span.attributes.items():
attr_v = self._normalize_attribute_value(attr_v)
Expand Down Expand Up @@ -153,6 +156,23 @@ def _add_info_instrumentation_scope(self, span, evt) -> None:
INTL_SWO_OTEL_SCOPE_VERSION, span.instrumentation_scope.version
)

def _add_info_status(self, span, evt) -> None:
"""Add info from span status, if exists"""
# Only set if not "UNSET", e.g. "OK", "ERROR"
if (
span.status.status_code
and span.status.status_code != StatusCode.UNSET
):
evt.addInfo(
INTL_SWO_OTEL_STATUS_CODE, span.status.status_code.name
)

# Only set if has value
if span.status.description:
evt.addInfo(
INTL_SWO_OTEL_STATUS_DESCRIPTION, span.status.description
)

# pylint: disable=too-many-branches,too-many-statements
def _add_info_instrumented_framework(self, span, evt) -> None:
"""Add info to span for which Python framework has been instrumented
Expand Down
207 changes: 205 additions & 2 deletions tests/unit/test_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
import solarwinds_apm.extension.oboe


# Little helper =====================================================
# Little helpers =====================================================

class FooNum(Enum):
FOO = "foo"

class MockStatus(Enum):
UNSET = 0
OK = 1
ERROR = 2


# Mock Fixtures =====================================================

Expand Down Expand Up @@ -48,6 +53,13 @@ def get_mock_spans(mocker, valid_parent=False):
"version": "foo.bar.baz",
}
)
mock_status = mocker.Mock()
mock_status.configure_mock(
**{
"status_code": FooNum.FOO,
"description": "foo-bar-baz",
}
)
mock_span = mocker.Mock()
mock_span_context = mocker.Mock()
mock_span_context.configure_mock(
Expand All @@ -69,6 +81,7 @@ def get_mock_spans(mocker, valid_parent=False):
mock_exception_event,
],
"instrumentation_scope": mock_instrumentation_scope,
"status": mock_status,
}
mock_parent = None
if valid_parent:
Expand Down Expand Up @@ -365,6 +378,8 @@ def assert_export_add_info_and_report(
mocker.call(solarwinds_apm.exporter.SolarWindsSpanExporter._SW_SPAN_NAME, "foo"),
mocker.call(solarwinds_apm.exporter.SolarWindsSpanExporter._SW_SPAN_KIND, FooNum.FOO.name),
mocker.call("Language", "Python"),
mocker.call("otel.status_code", "FOO"),
mocker.call("otel.status_description", "foo-bar-baz"),
mocker.call("foo", "bar"),
mocker.call("Layer", "FOO:foo"),
])
Expand Down Expand Up @@ -422,7 +437,16 @@ def test_export_root_span(
mock_add_info_instr_fwork,
mock_md,
mock_spans_root
):
):
mock_statuscode = mocker.patch(
"solarwinds_apm.exporter.StatusCode"
)
mock_statuscode.configure_mock(
**{
"ERROR": "foo-code",
"OK": "bar-code",
}
)
mock_build_md = mocker.patch(
"solarwinds_apm.exporter.SolarWindsSpanExporter._build_metadata",
return_value=mock_md
Expand Down Expand Up @@ -461,6 +485,15 @@ def test_export_parent_valid(
mock_md,
mock_spans_parent_valid
):
mock_statuscode = mocker.patch(
"solarwinds_apm.exporter.StatusCode"
)
mock_statuscode.configure_mock(
**{
"ERROR": "foo-code",
"OK": "bar-code",
}
)
mock_build_md = mocker.patch(
"solarwinds_apm.exporter.SolarWindsSpanExporter._build_metadata",
return_value=mock_md
Expand Down Expand Up @@ -698,6 +731,176 @@ def test__add_info_instrumentation_scope_name_and_version(
mocker.call("otel.scope.version", "bar"),
])

def test___add_info_status_none(
self,
mocker,
exporter,
mock_event,
mock_create_event,
):
mocker.patch(
"solarwinds_apm.exporter.StatusCode",
return_value=MockStatus,
)

# mock liboboe event
mock_event, mock_add_info, _ \
= configure_event_mocks(
mocker,
mock_event,
mock_create_event,
True,
)
# mock otel span with status
mock_status = mocker.Mock()
mock_status.configure_mock(
**{
"status_code": None,
"description": None,
}
)
test_span = mocker.Mock()
test_span.configure_mock(
**{
"status": mock_status,
}
)

exporter._add_info_status(
test_span,
mock_event,
)
mock_add_info.assert_not_called()

def test___add_info_status_unset_code(
self,
mocker,
exporter,
mock_event,
mock_create_event,
):
mock_statuscode = mocker.patch(
"solarwinds_apm.exporter.StatusCode",
return_value=MockStatus,
)

# mock liboboe event
mock_event, mock_add_info, _ \
= configure_event_mocks(
mocker,
mock_event,
mock_create_event,
True,
)
# mock otel span with status
mock_status = mocker.Mock()
mock_status.configure_mock(
**{
"status_code": mock_statuscode.UNSET,
"description": None,
}
)
test_span = mocker.Mock()
test_span.configure_mock(
**{
"status": mock_status,
}
)

exporter._add_info_status(
test_span,
mock_event,
)
mock_add_info.assert_not_called()

def test___add_info_status_ok_code(
self,
mocker,
exporter,
mock_event,
mock_create_event,
):
mocker.patch(
"solarwinds_apm.exporter.StatusCode",
return_value=MockStatus,
)

# mock liboboe event
mock_event, mock_add_info, _ \
= configure_event_mocks(
mocker,
mock_event,
mock_create_event,
True,
)
# mock otel span with status
mock_status = mocker.Mock()
mock_status.configure_mock(
**{
"status_code": MockStatus.OK,
"description": "blah blah blah",
}
)
test_span = mocker.Mock()
test_span.configure_mock(
**{
"status": mock_status,
}
)

exporter._add_info_status(
test_span,
mock_event,
)
mock_add_info.assert_has_calls([
mocker.call("otel.status_code", "OK"),
mocker.call("otel.status_description", "blah blah blah"),
])

def test___add_info_status_error_code(
self,
mocker,
exporter,
mock_event,
mock_create_event,
):
mocker.patch(
"solarwinds_apm.exporter.StatusCode",
return_value=MockStatus,
)

# mock liboboe event
mock_event, mock_add_info, _ \
= configure_event_mocks(
mocker,
mock_event,
mock_create_event,
True,
)
# mock otel span with status
mock_status = mocker.Mock()
mock_status.configure_mock(
**{
"status_code": MockStatus.ERROR,
"description": "blah blah blah",
}
)
test_span = mocker.Mock()
test_span.configure_mock(
**{
"status": mock_status,
}
)

exporter._add_info_status(
test_span,
mock_event,
)
mock_add_info.assert_has_calls([
mocker.call("otel.status_code", "ERROR"),
mocker.call("otel.status_description", "blah blah blah"),
])

def mock_and_assert_addinfo_for_instrumented_framework(
self,
mocker,
Expand Down

0 comments on commit 8676416

Please sign in to comment.