Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 1.0.0b47 (Unreleased)

### Features Added
- Add support for user id and authId
([#44662](https://github.com/Azure/azure-sdk-for-python/pull/44662))

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,35 @@
from typing import Optional, Sequence, Any

from opentelemetry._logs.severity import SeverityNumber
from opentelemetry.sdk._logs import ReadableLogRecord
from opentelemetry.sdk._logs.export import LogRecordExporter, LogRecordExportResult
from opentelemetry.semconv.attributes.exception_attributes import (
EXCEPTION_ESCAPED,
EXCEPTION_MESSAGE,
EXCEPTION_STACKTRACE,
EXCEPTION_TYPE,
)
from opentelemetry.sdk._logs import ReadableLogRecord
from opentelemetry.sdk._logs.export import LogRecordExporter, LogRecordExportResult

try:
from opentelemetry.semconv.logs import (
LogRecordAttributes as _SemconvLogRecordAttributes,
)
except ImportError:
_SemconvLogRecordAttributes = None
try:
from opentelemetry.semconv._incubating.attributes import (
enduser_attributes as _enduser_attributes,
)
except ImportError:
_enduser_attributes = None

from azure.monitor.opentelemetry.exporter import _utils
from azure.monitor.opentelemetry.exporter._constants import (
_APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE,
_DEFAULT_LOG_MESSAGE,
_EXCEPTION_ENVELOPE_NAME,
_MESSAGE_ENVELOPE_NAME,
_DEFAULT_LOG_MESSAGE,
_MICROSOFT_CUSTOM_EVENT_NAME,
)
from azure.monitor.opentelemetry.exporter._generated.models import (
ContextTagKeys,
Expand All @@ -34,17 +49,24 @@
ExportResult,
)
from azure.monitor.opentelemetry.exporter.export.trace import _utils as trace_utils
from azure.monitor.opentelemetry.exporter._constants import (
_APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE,
_MICROSOFT_CUSTOM_EVENT_NAME,
)
from azure.monitor.opentelemetry.exporter.statsbeat._state import (
get_statsbeat_shutdown,
get_statsbeat_custom_events_feature_set,
is_statsbeat_enabled,
set_statsbeat_custom_events_feature_set,
)

_ENDUSER_ID_ATTRIBUTE = (
getattr(_SemconvLogRecordAttributes, "ENDUSER_ID", None)
or getattr(_enduser_attributes, "ENDUSER_ID", None)
or "enduser.id"
)
_ENDUSER_PSEUDO_ID_ATTRIBUTE = (
getattr(_SemconvLogRecordAttributes, "ENDUSER_PSEUDO_ID", None)
or getattr(_enduser_attributes, "ENDUSER_PSEUDO_ID", None)
or "enduser.pseudo.id"
)

_logger = logging.getLogger(__name__)

_DEFAULT_SPAN_ID = 0
Expand Down Expand Up @@ -123,27 +145,32 @@ def _convert_log_to_envelope(readable_log_record: ReadableLogRecord) -> Telemetr
log_record = readable_log_record.log_record
time_stamp = log_record.timestamp if log_record.timestamp is not None else log_record.observed_timestamp
envelope = _utils._create_telemetry_item(time_stamp)
envelope.tags.update(_utils._populate_part_a_fields(readable_log_record.resource)) # type: ignore
envelope.tags[ContextTagKeys.AI_OPERATION_ID] = "{:032x}".format( # type: ignore
log_record.trace_id or _DEFAULT_TRACE_ID
)
envelope.tags[ContextTagKeys.AI_OPERATION_PARENT_ID] = "{:016x}".format( # type: ignore
tags = envelope.tags or {}
envelope.tags = tags
tags.update(_utils._populate_part_a_fields(readable_log_record.resource)) # type: ignore
tags[ContextTagKeys.AI_OPERATION_ID] = "{:032x}".format(log_record.trace_id or _DEFAULT_TRACE_ID) # type: ignore
if log_record.attributes and _ENDUSER_ID_ATTRIBUTE in log_record.attributes:
tags[ContextTagKeys.AI_USER_AUTH_USER_ID] = log_record.attributes[_ENDUSER_ID_ATTRIBUTE]
if log_record.attributes and _ENDUSER_PSEUDO_ID_ATTRIBUTE in log_record.attributes:
tags[ContextTagKeys.AI_USER_ID] = log_record.attributes[_ENDUSER_PSEUDO_ID_ATTRIBUTE]

tags[ContextTagKeys.AI_OPERATION_PARENT_ID] = "{:016x}".format( # type: ignore
log_record.span_id or _DEFAULT_SPAN_ID
)
if (
log_record.attributes
and ContextTagKeys.AI_OPERATION_NAME in log_record.attributes
and log_record.attributes[ContextTagKeys.AI_OPERATION_NAME] is not None
):
envelope.tags[ContextTagKeys.AI_OPERATION_NAME] = log_record.attributes.get( # type: ignore
tags[ContextTagKeys.AI_OPERATION_NAME] = log_record.attributes.get( # type: ignore
ContextTagKeys.AI_OPERATION_NAME
)
if _utils._is_any_synthetic_source(log_record.attributes):
envelope.tags[ContextTagKeys.AI_OPERATION_SYNTHETIC_SOURCE] = "True" # type: ignore
tags[ContextTagKeys.AI_OPERATION_SYNTHETIC_SOURCE] = "True" # type: ignore
# Special use case: Customers want to be able to set location ip on log records
location_ip = trace_utils._get_location_ip(log_record.attributes)
if location_ip:
envelope.tags[ContextTagKeys.AI_LOCATION_IP] = location_ip # type: ignore
tags[ContextTagKeys.AI_LOCATION_IP] = location_ip # type: ignore
properties = _utils._filter_custom_properties(
log_record.attributes, lambda key, val: not _is_ignored_attribute(key) # type: ignore
)
Expand Down Expand Up @@ -236,7 +263,7 @@ def _map_body_to_message(log_body: Any) -> str:

try:
return json.dumps(log_body)[:32768]
except: # pylint: disable=bare-except
except Exception: # pylint: disable=broad-except
return str(log_body)[:32768]


Expand All @@ -252,6 +279,8 @@ def _is_ignored_attribute(key: str) -> bool:
EXCEPTION_ESCAPED,
_APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE,
_MICROSOFT_CUSTOM_EVENT_NAME,
_ENDUSER_ID_ATTRIBUTE,
_ENDUSER_PSEUDO_ID_ATTRIBUTE,
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
)
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
from opentelemetry.semconv._incubating.attributes import gen_ai_attributes

try:
from opentelemetry.semconv._incubating.attributes import (
enduser_attributes as _enduser_attributes,
)
except ImportError:
_enduser_attributes = None
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
Expand Down Expand Up @@ -56,6 +63,17 @@

__all__ = ["AzureMonitorTraceExporter"]

_ENDUSER_ID_ATTRIBUTE = (
getattr(SpanAttributes, "ENDUSER_ID", None)
or (getattr(_enduser_attributes, "ENDUSER_ID", None) if _enduser_attributes is not None else None)
or "enduser.id"
)
_ENDUSER_PSEUDO_ID_ATTRIBUTE = (
getattr(SpanAttributes, "ENDUSER_PSEUDO_ID", None)
or (getattr(_enduser_attributes, "ENDUSER_PSEUDO_ID", None) if _enduser_attributes is not None else None)
or "enduser.pseudo.id"
)

_STANDARD_OPENTELEMETRY_ATTRIBUTE_PREFIXES = [
"http.",
"db.",
Expand Down Expand Up @@ -107,9 +125,7 @@ def __init__(self, **kwargs: Any):
self._tracer_provider = kwargs.pop("tracer_provider", None)
super().__init__(**kwargs)

def export(
self, spans: Sequence[ReadableSpan], **kwargs: Any # pylint: disable=unused-argument
) -> SpanExportResult:
def export(self, spans: Sequence[ReadableSpan], **_kwargs: Any) -> SpanExportResult:
"""Export span data.

:param spans: Open Telemetry Spans to export.
Expand Down Expand Up @@ -222,8 +238,10 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem:
envelope = _utils._create_telemetry_item(start_time)
envelope.tags.update(_utils._populate_part_a_fields(span.resource))
envelope.tags[ContextTagKeys.AI_OPERATION_ID] = "{:032x}".format(span.context.trace_id)
if SpanAttributes.ENDUSER_ID in span.attributes:
envelope.tags[ContextTagKeys.AI_USER_ID] = span.attributes[SpanAttributes.ENDUSER_ID]
if _ENDUSER_ID_ATTRIBUTE in span.attributes:
envelope.tags[ContextTagKeys.AI_USER_AUTH_USER_ID] = span.attributes[_ENDUSER_ID_ATTRIBUTE]
if _ENDUSER_PSEUDO_ID_ATTRIBUTE in span.attributes:
envelope.tags[ContextTagKeys.AI_USER_ID] = span.attributes[_ENDUSER_PSEUDO_ID_ATTRIBUTE]
if _utils._is_any_synthetic_source(span.attributes):
envelope.tags[ContextTagKeys.AI_OPERATION_SYNTHETIC_SOURCE] = "True"
if span.parent and span.parent.span_id:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,26 @@ def setUpClass(cls):
severity_text="WARNING",
severity_number=SeverityNumber.WARN,
body="Test message",
attributes={"test": "attribute", "ai.operation.name": "TestOperationName"},
attributes={
"test": "attribute",
"ai.operation.name": "TestOperationName",
},
),
resource=Resource.create(attributes={"asd": "test_resource"}),
instrumentation_scope=InstrumentationScope("test_name"),
)
cls._log_data_user_fields = _logs.ReadWriteLogRecord(
LogRecord(
timestamp=1646865018558419456,
context=ctx,
severity_text="WARNING",
severity_number=SeverityNumber.WARN,
body="Test message",
attributes={
"test": "attribute",
"enduser.id": "test-auth",
"enduser.pseudo.id": "test-user",
},
),
resource=Resource.create(attributes={"asd": "test_resource"}),
instrumentation_scope=InstrumentationScope("test_name"),
Expand All @@ -92,7 +111,10 @@ def setUpClass(cls):
severity_text="WARNING",
severity_number=SeverityNumber.WARN,
body="",
attributes={"test": "attribute", "ai.operation.name": "TestOperationName"},
attributes={
"test": "attribute",
"ai.operation.name": "TestOperationName",
},
),
resource=Resource.create(attributes={"asd": "test_resource"}),
instrumentation_scope=InstrumentationScope("test_name"),
Expand All @@ -116,7 +138,10 @@ def setUpClass(cls):
severity_text="WARNING",
severity_number=SeverityNumber.WARN,
body={"foo": {"bar": "baz", "qux": 42}},
attributes={"test": "attribute", "ai.operation.name": "TestOperationName"},
attributes={
"test": "attribute",
"ai.operation.name": "TestOperationName",
},
),
resource=Resource.create(attributes={"asd": "test_resource"}),
instrumentation_scope=InstrumentationScope("test_name"),
Expand Down Expand Up @@ -246,7 +271,12 @@ def setUpClass(cls):
severity_text="EXCEPTION",
severity_number=SeverityNumber.FATAL,
body="test exception",
attributes={"test": "attribute", EXCEPTION_TYPE: "", EXCEPTION_MESSAGE: "", EXCEPTION_STACKTRACE: ""},
attributes={
"test": "attribute",
EXCEPTION_TYPE: "",
EXCEPTION_MESSAGE: "",
EXCEPTION_STACKTRACE: "",
},
),
resource=Resource.create(attributes={"asd": "test_resource"}),
instrumentation_scope=InstrumentationScope("test_name"),
Expand All @@ -258,7 +288,12 @@ def setUpClass(cls):
severity_text="EXCEPTION",
severity_number=SeverityNumber.FATAL,
body="",
attributes={"test": "attribute", EXCEPTION_TYPE: "", EXCEPTION_MESSAGE: "", EXCEPTION_STACKTRACE: ""},
attributes={
"test": "attribute",
EXCEPTION_TYPE: "",
EXCEPTION_MESSAGE: "",
EXCEPTION_STACKTRACE: "",
},
),
resource=Resource.create(attributes={"asd": "test_resource"}),
instrumentation_scope=InstrumentationScope("test_name"),
Expand Down Expand Up @@ -354,26 +389,44 @@ def test_log_to_envelope_partA(self):
self.assertEqual(envelope.instrumentation_key, "1234abcd-5678-4efa-8abc-1234567890ab")
self.assertIsNotNone(envelope.tags)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_DEVICE_ID), azure_monitor_context[ContextTagKeys.AI_DEVICE_ID]
envelope.tags.get(ContextTagKeys.AI_DEVICE_ID),
azure_monitor_context[ContextTagKeys.AI_DEVICE_ID],
)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_DEVICE_LOCALE), azure_monitor_context[ContextTagKeys.AI_DEVICE_LOCALE]
envelope.tags.get(ContextTagKeys.AI_DEVICE_LOCALE),
azure_monitor_context[ContextTagKeys.AI_DEVICE_LOCALE],
)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_DEVICE_TYPE), azure_monitor_context[ContextTagKeys.AI_DEVICE_TYPE]
envelope.tags.get(ContextTagKeys.AI_DEVICE_TYPE),
azure_monitor_context[ContextTagKeys.AI_DEVICE_TYPE],
)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_INTERNAL_SDK_VERSION),
azure_monitor_context[ContextTagKeys.AI_INTERNAL_SDK_VERSION],
)

self.assertEqual(envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE), "testServiceNamespace.testServiceName")
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE), "testServiceInstanceId")
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_INTERNAL_NODE_NAME), "testServiceInstanceId")
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE),
"testServiceNamespace.testServiceName",
)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE),
"testServiceInstanceId",
)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_INTERNAL_NODE_NAME),
"testServiceInstanceId",
)
trace_id = self._log_data.log_record.trace_id
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_OPERATION_ID), "{:032x}".format(trace_id))
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_OPERATION_ID),
"{:032x}".format(trace_id),
)
span_id = self._log_data.log_record.span_id
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_OPERATION_PARENT_ID), "{:016x}".format(span_id))
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_OPERATION_PARENT_ID),
"{:016x}".format(span_id),
)
self._log_data.resource = old_resource
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_OPERATION_NAME), "TestOperationName")

Expand Down Expand Up @@ -402,6 +455,15 @@ def test_log_to_envelope_log(self):
self.assertEqual(envelope.data.base_data.severity_level, 2)
self.assertEqual(envelope.data.base_data.properties["test"], "attribute")

def test_log_to_envelope_user_fields(self):
exporter = self._exporter
envelope = exporter._log_to_envelope(self._log_data_user_fields)

self.assertEqual(envelope.tags.get(ContextTagKeys.AI_USER_AUTH_USER_ID), "test-auth")
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_USER_ID), "test-user")
self.assertNotIn("enduser.id", envelope.data.base_data.properties)
self.assertNotIn("enduser.pseudo.id", envelope.data.base_data.properties)

def test_log_to_envelope_log_none(self):
exporter = self._exporter
envelope = exporter._log_to_envelope(self._log_data_none)
Expand Down Expand Up @@ -429,7 +491,10 @@ def test_log_to_envelope_log_complex_body(self):
envelope = exporter._log_to_envelope(self._log_data_complex_body)
self.assertEqual(envelope.name, "Microsoft.ApplicationInsights.Message")
self.assertEqual(envelope.data.base_type, "MessageData")
self.assertEqual(envelope.data.base_data.message, json.dumps(self._log_data_complex_body.log_record.body))
self.assertEqual(
envelope.data.base_data.message,
json.dumps(self._log_data_complex_body.log_record.body),
)
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_OPERATION_NAME), "TestOperationName")

def test_log_to_envelope_log_complex_body_not_serializeable(self):
Expand All @@ -438,7 +503,8 @@ def test_log_to_envelope_log_complex_body_not_serializeable(self):
self.assertEqual(envelope.name, "Microsoft.ApplicationInsights.Message")
self.assertEqual(envelope.data.base_type, "MessageData")
self.assertEqual(
envelope.data.base_data.message, str(self._log_data_complex_body_not_serializeable.log_record.body)
envelope.data.base_data.message,
str(self._log_data_complex_body_not_serializeable.log_record.body),
)

def test_log_to_envelope_exception_with_string_message(self):
Expand Down Expand Up @@ -593,8 +659,14 @@ def test_log_to_envelope_synthetic_source(self):
envelope = exporter._log_to_envelope(log_data)

self.assertEqual(envelope.tags.get(ContextTagKeys.AI_OPERATION_SYNTHETIC_SOURCE), "True")
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE), "testServiceNamespace.testServiceName")
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE), "testServiceInstanceId")
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE),
"testServiceNamespace.testServiceName",
)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE),
"testServiceInstanceId",
)

def test_log_to_envelope_synthetic_load_always_on(self):
exporter = self._exporter
Expand Down Expand Up @@ -631,8 +703,14 @@ def test_log_to_envelope_synthetic_load_always_on(self):
envelope = exporter._log_to_envelope(log_data)

self.assertEqual(envelope.tags.get(ContextTagKeys.AI_OPERATION_SYNTHETIC_SOURCE), "True")
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE), "testServiceNamespace.testServiceName")
self.assertEqual(envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE), "testServiceInstanceId")
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE),
"testServiceNamespace.testServiceName",
)
self.assertEqual(
envelope.tags.get(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE),
"testServiceInstanceId",
)


class TestAzureLogExporterWithDisabledStorage(TestAzureLogExporter):
Expand Down
Loading