Skip to content

Commit bc31f16

Browse files
authored
Implement exporting span events as message/exception telemetry (Azure#23708)
1 parent c7df407 commit bc31f16

File tree

8 files changed

+229
-21
lines changed

8 files changed

+229
-21
lines changed

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
([#23486](https://github.com/Azure/azure-sdk-for-python/pull/23486))
88
- Implement sending of exception telemetry via log exporter
99
([#23633](https://github.com/Azure/azure-sdk-for-python/pull/23633))
10-
10+
- Implement exporting span events as message/exception telemetry
11+
([#23708](https://github.com/Azure/azure-sdk-for-python/pull/23708))
1112

1213
### Breaking Changes
1314

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import pkg_resources
99

1010
from opentelemetry.semconv.resource import ResourceAttributes
11+
from opentelemetry.sdk.util import ns_to_iso_str
1112

13+
from azure.monitor.opentelemetry.exporter._generated.models import TelemetryItem
1214
from azure.monitor.opentelemetry.exporter._version import VERSION as ext_version
1315

1416

@@ -74,6 +76,14 @@ def run(self):
7476
def cancel(self):
7577
self.finished.set()
7678

79+
def _create_telemetry_item(timestamp):
80+
return TelemetryItem(
81+
name="",
82+
instrumentation_key="",
83+
tags=dict(azure_monitor_context),
84+
time=ns_to_iso_str(timestamp),
85+
)
86+
7787
def _populate_part_a_fields(resource):
7888
tags = {}
7989
if resource and resource.attributes:

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/logs/_exporter.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from opentelemetry.sdk._logs import LogData
88
from opentelemetry.sdk._logs.severity import SeverityNumber
99
from opentelemetry.sdk._logs.export import LogExporter, LogExportResult
10-
from opentelemetry.sdk.util import ns_to_iso_str
1110

1211
from azure.monitor.opentelemetry.exporter import _utils
1312
from azure.monitor.opentelemetry.exporter._generated.models import (
@@ -90,12 +89,7 @@ def from_connection_string(
9089
# pylint: disable=protected-access
9190
def _convert_log_to_envelope(log_data: LogData) -> TelemetryItem:
9291
log_record = log_data.log_record
93-
envelope = TelemetryItem(
94-
name="",
95-
instrumentation_key="",
96-
tags=dict(_utils.azure_monitor_context),
97-
time=ns_to_iso_str(log_record.timestamp),
98-
)
92+
envelope = _utils._create_telemetry_item(log_record.timestamp)
9993
envelope.tags.update(_utils._populate_part_a_fields(log_record.resource))
10094
envelope.tags["ai.operation.id"] = "{:032x}".format(
10195
log_record.trace_id or _DEFAULT_TRACE_ID
@@ -118,6 +112,8 @@ def _convert_log_to_envelope(log_data: LogData) -> TelemetryItem:
118112
if exc_type is not None or exc_message is not None:
119113
envelope.name = "Microsoft.ApplicationInsights.Exception"
120114
has_full_stack = stack_trace is not None
115+
if not exc_message:
116+
exc_message = "Exception"
121117
exc_details = TelemetryExceptionDetails(
122118
type_name=exc_type,
123119
message=exc_message,
@@ -130,7 +126,7 @@ def _convert_log_to_envelope(log_data: LogData) -> TelemetryItem:
130126
exceptions=[exc_details],
131127
)
132128
# pylint: disable=line-too-long
133-
envelope.data = MonitorBase(base_data=data, base_type="TelemetryExceptionData")
129+
envelope.data = MonitorBase(base_data=data, base_type="ExceptionData")
134130
else: # Message telemetry
135131
envelope.name = "Microsoft.ApplicationInsights.Message"
136132
# pylint: disable=line-too-long

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_exporter.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77

88
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
99
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
10-
from opentelemetry.sdk.util import ns_to_iso_str
1110
from opentelemetry.trace import Span, SpanKind
1211

1312
from azure.monitor.opentelemetry.exporter import _utils
1413
from azure.monitor.opentelemetry.exporter._generated.models import (
14+
MessageData,
1515
MonitorBase,
1616
RemoteDependencyData,
1717
RequestData,
18+
TelemetryExceptionData,
19+
TelemetryExceptionDetails,
1820
TelemetryItem
1921
)
2022
from azure.monitor.opentelemetry.exporter.export._base import (
@@ -36,7 +38,10 @@ def export(self, spans: Sequence[Span], **kwargs: Any) -> SpanExportResult: # py
3638
:type spans: Sequence[~opentelemetry.trace.Span]
3739
:rtype: ~opentelemetry.sdk.trace.export.SpanExportResult
3840
"""
39-
envelopes = [self._span_to_envelope(span) for span in spans]
41+
envelopes = []
42+
for span in spans:
43+
envelopes.append(self._span_to_envelope(span))
44+
envelopes.extend(self._span_events_to_envelopes(span))
4045
try:
4146
result = self._transmit(envelopes)
4247
if result == ExportResult.FAILED_RETRYABLE:
@@ -64,6 +69,14 @@ def _span_to_envelope(self, span: Span) -> TelemetryItem:
6469
envelope.instrumentation_key = self._instrumentation_key
6570
return envelope
6671

72+
def _span_events_to_envelopes(self, span: Span) -> Sequence[TelemetryItem]:
73+
if not span or len(span.events) == 0:
74+
return []
75+
envelopes = _convert_span_events_to_envelopes(span)
76+
for envelope in envelopes:
77+
envelope.instrumentation_key = self._instrumentation_key
78+
return envelopes
79+
6780
@classmethod
6881
def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "AzureMonitorTraceExporter":
6982
"""
@@ -82,24 +95,17 @@ def from_connection_string(cls, conn_str: str, **kwargs: Any) -> "AzureMonitorTr
8295
# pylint: disable=too-many-statements
8396
# pylint: disable=too-many-branches
8497
# pylint: disable=too-many-locals
98+
# pylint: disable=protected-access
8599
def _convert_span_to_envelope(span: Span) -> TelemetryItem:
86-
envelope = TelemetryItem(
87-
name="",
88-
instrumentation_key="",
89-
tags=dict(_utils.azure_monitor_context),
90-
time=ns_to_iso_str(span.start_time),
91-
)
92-
# pylint: disable=protected-access
100+
envelope = _utils._create_telemetry_item(span.start_time)
93101
envelope.tags.update(_utils._populate_part_a_fields(span.resource))
94-
95102
envelope.tags["ai.operation.id"] = "{:032x}".format(span.context.trace_id)
96103
if SpanAttributes.ENDUSER_ID in span.attributes:
97104
envelope.tags["ai.user.id"] = span.attributes[SpanAttributes.ENDUSER_ID]
98105
if span.parent and span.parent.span_id:
99106
envelope.tags["ai.operation.parentId"] = "{:016x}".format(
100107
span.parent.span_id
101108
)
102-
103109
# pylint: disable=too-many-nested-blocks
104110
if span.kind in (SpanKind.CONSUMER, SpanKind.SERVER):
105111
envelope.name = "Microsoft.ApplicationInsights.Request"
@@ -382,6 +388,52 @@ def _convert_span_to_envelope(span: Span) -> TelemetryItem:
382388
data.properties["_MS.links"] = json.dumps(links)
383389
return envelope
384390

391+
# pylint: disable=protected-access
392+
def _convert_span_events_to_envelopes(span: Span) -> Sequence[TelemetryItem]:
393+
envelopes = []
394+
for event in span.events:
395+
envelope = _utils._create_telemetry_item(event.timestamp)
396+
envelope.tags.update(_utils._populate_part_a_fields(span.resource))
397+
envelope.tags["ai.operation.id"] = "{:032x}".format(span.context.trace_id)
398+
if span.parent and span.parent.span_id:
399+
envelope.tags["ai.operation.parentId"] = "{:016x}".format(
400+
span.parent.span_id
401+
)
402+
properties = {}
403+
if event.name == "exception":
404+
envelope.name = 'Microsoft.ApplicationInsights.Exception'
405+
exc_type = event.attributes.get(SpanAttributes.EXCEPTION_TYPE)
406+
exc_message = event.attributes.get(SpanAttributes.EXCEPTION_MESSAGE)
407+
if exc_message is None or not exc_message:
408+
exc_message = "Exception"
409+
stack_trace = event.attributes.get(SpanAttributes.EXCEPTION_STACKTRACE)
410+
escaped = event.attributes.get(SpanAttributes.EXCEPTION_ESCAPED)
411+
properties[SpanAttributes.EXCEPTION_ESCAPED] = escaped
412+
has_full_stack = stack_trace is not None
413+
exc_details = TelemetryExceptionDetails(
414+
type_name=exc_type,
415+
message=exc_message,
416+
has_full_stack=has_full_stack,
417+
stack=stack_trace,
418+
)
419+
data = TelemetryExceptionData(
420+
properties=properties,
421+
exceptions=[exc_details],
422+
)
423+
# pylint: disable=line-too-long
424+
envelope.data = MonitorBase(base_data=data, base_type='ExceptionData')
425+
else:
426+
envelope.name = 'Microsoft.ApplicationInsights.Message'
427+
properties.update(event.attributes)
428+
data = MessageData(
429+
message=event.name,
430+
properties=properties,
431+
)
432+
envelope.data = MonitorBase(base_data=data, base_type='MessageData')
433+
434+
envelopes.append(envelope)
435+
436+
return envelopes
385437

386438
# pylint:disable=too-many-return-statements
387439
def _get_default_port_db(dbsystem):

sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ These code samples show common champion scenario operations with the AzureMonito
1414
* Azure Service Bus Receive: [sample_servicebus_receive.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/sample_servicebus_receive.py)
1515
* Azure Storage Blob Create Container: [sample_storage.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/sample_storage.py)
1616
* Client: [sample_client.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/sample_client.py)
17+
* Event: [sample_event.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/sample_event.py)
1718
* Jaeger: [sample_jaeger.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/sample_jaeger.py)
1819
* Trace: [sample_trace.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/sample_trace.py)
1920
* Server: [sample_server.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/traces/sample_server.py)
@@ -49,6 +50,17 @@ $ # from this directory
4950
$ python sample_request.py
5051
```
5152

53+
### Event
54+
55+
* Update `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable
56+
57+
* Run the sample
58+
59+
```sh
60+
$ # from this directory
61+
$ python sample_event.py
62+
```
63+
5264
### Server
5365

5466
* Update `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""
4+
An example to show an application using custom events. Events are added
5+
to the span and exported via the AzureMonitorTraceExporter.
6+
"""
7+
import os
8+
from opentelemetry import trace
9+
from opentelemetry.sdk.trace import TracerProvider
10+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
11+
12+
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
13+
14+
exporter = AzureMonitorTraceExporter.from_connection_string(
15+
os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
16+
)
17+
18+
trace.set_tracer_provider(TracerProvider())
19+
tracer = trace.get_tracer(__name__)
20+
span_processor = BatchSpanProcessor(exporter)
21+
trace.get_tracer_provider().add_span_processor(span_processor)
22+
23+
# Message events
24+
with tracer.start_as_current_span("hello") as span:
25+
span.add_event("Custom event", {"test": "attributes"})
26+
print("Hello, World!")
27+
28+
# Exception events
29+
try:
30+
with tracer.start_as_current_span("hello") as span:
31+
raise Exception("Custom exception message.")
32+
except Exception:
33+
print("Exception raised")

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/logs/test_logs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def test_log_to_envelope_log(self):
208208
def test_log_to_envelope_exception(self):
209209
exporter = self._exporter
210210
envelope = exporter._log_to_envelope(self._exc_data)
211-
self.assertEqual(envelope.data.base_type, 'TelemetryExceptionData')
211+
self.assertEqual(envelope.data.base_type, 'ExceptionData')
212212
self.assertEqual(envelope.data.base_data.severity_level, 4)
213213
self.assertEqual(envelope.data.base_data.properties["test"], "attribute")
214214
self.assertEqual(len(envelope.data.base_data.exceptions), 1)

sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# pylint: disable=import-error
1313
from opentelemetry.sdk import trace, resources
1414
from opentelemetry.sdk.trace.export import SpanExportResult
15+
from opentelemetry.semconv.trace import SpanAttributes
1516
from opentelemetry.trace import Link, SpanContext, SpanKind
1617
from opentelemetry.trace.status import Status, StatusCode
1718

@@ -775,6 +776,109 @@ def test_span_to_envelope_properties(self):
775776
)[0]
776777
self.assertEqual(json_dict["id"], "a6f5d48acb4d31da")
777778

779+
def test_span_events_to_envelopes_exception(self):
780+
exporter = self._exporter
781+
time = 1575494316027613500
782+
783+
span = trace._Span(
784+
name="test",
785+
context=SpanContext(
786+
trace_id=36873507687745823477771305566750195431,
787+
span_id=12030755672171557337,
788+
is_remote=False,
789+
),
790+
parent=SpanContext(
791+
trace_id=36873507687745823477771305566750195432,
792+
span_id=12030755672171557337,
793+
is_remote=False,
794+
),
795+
kind=SpanKind.CLIENT,
796+
)
797+
attributes = {
798+
SpanAttributes.EXCEPTION_TYPE: "ZeroDivisionError",
799+
SpanAttributes.EXCEPTION_MESSAGE: "zero division error",
800+
SpanAttributes.EXCEPTION_STACKTRACE: "Traceback: ZeroDivisionError, division by zero",
801+
SpanAttributes.EXCEPTION_ESCAPED: "True",
802+
}
803+
span.add_event("exception", attributes, time)
804+
span.start()
805+
span.end()
806+
span._status = Status(status_code=StatusCode.OK)
807+
envelopes = exporter._span_events_to_envelopes(span)
808+
809+
self.assertEqual(len(envelopes), 1)
810+
envelope = envelopes[0]
811+
self.assertEqual(
812+
envelope.name, "Microsoft.ApplicationInsights.Exception"
813+
)
814+
self.assertEqual(envelope.instrumentation_key,
815+
"1234abcd-5678-4efa-8abc-1234567890ab")
816+
self.assertIsNotNone(envelope.tags)
817+
self.assertEqual(envelope.tags.get("ai.device.id"), azure_monitor_context["ai.device.id"])
818+
self.assertEqual(envelope.tags.get("ai.device.locale"), azure_monitor_context["ai.device.locale"])
819+
self.assertEqual(envelope.tags.get("ai.device.osVersion"), azure_monitor_context["ai.device.osVersion"])
820+
self.assertEqual(envelope.tags.get("ai.device.type"), azure_monitor_context["ai.device.type"])
821+
self.assertEqual(envelope.tags.get("ai.internal.sdkVersion"), azure_monitor_context["ai.internal.sdkVersion"])
822+
self.assertEqual(envelope.tags.get("ai.operation.id"), "{:032x}".format(span.context.trace_id))
823+
self.assertEqual(envelope.tags.get("ai.operation.parentId"), "{:016x}".format(span.context.span_id))
824+
self.assertEqual(envelope.time, "2019-12-04T21:18:36.027613Z")
825+
self.assertEqual(len(envelope.data.base_data.properties), 1)
826+
self.assertEqual(envelope.data.base_data.properties[SpanAttributes.EXCEPTION_ESCAPED], "True")
827+
self.assertEqual(len(envelope.data.base_data.exceptions), 1)
828+
self.assertEqual(envelope.data.base_data.exceptions[0].type_name, "ZeroDivisionError")
829+
self.assertEqual(envelope.data.base_data.exceptions[0].message, "zero division error")
830+
self.assertEqual(envelope.data.base_data.exceptions[0].has_full_stack, True)
831+
self.assertEqual(envelope.data.base_data.exceptions[0].stack, "Traceback: ZeroDivisionError, division by zero")
832+
self.assertEqual(envelope.data.base_type, "ExceptionData")
833+
834+
def test_span_events_to_envelopes_message(self):
835+
exporter = self._exporter
836+
time = 1575494316027613500
837+
838+
span = trace._Span(
839+
name="test",
840+
context=SpanContext(
841+
trace_id=36873507687745823477771305566750195431,
842+
span_id=12030755672171557337,
843+
is_remote=False,
844+
),
845+
parent=SpanContext(
846+
trace_id=36873507687745823477771305566750195432,
847+
span_id=12030755672171557337,
848+
is_remote=False,
849+
),
850+
kind=SpanKind.CLIENT,
851+
)
852+
attributes = {
853+
"test": "asd",
854+
}
855+
span.add_event("test event", attributes, time)
856+
span.start()
857+
span.end()
858+
span._status = Status(status_code=StatusCode.OK)
859+
envelopes = exporter._span_events_to_envelopes(span)
860+
861+
self.assertEqual(len(envelopes), 1)
862+
envelope = envelopes[0]
863+
self.assertEqual(
864+
envelope.name, "Microsoft.ApplicationInsights.Message"
865+
)
866+
self.assertEqual(envelope.instrumentation_key,
867+
"1234abcd-5678-4efa-8abc-1234567890ab")
868+
self.assertIsNotNone(envelope.tags)
869+
self.assertEqual(envelope.tags.get("ai.device.id"), azure_monitor_context["ai.device.id"])
870+
self.assertEqual(envelope.tags.get("ai.device.locale"), azure_monitor_context["ai.device.locale"])
871+
self.assertEqual(envelope.tags.get("ai.device.osVersion"), azure_monitor_context["ai.device.osVersion"])
872+
self.assertEqual(envelope.tags.get("ai.device.type"), azure_monitor_context["ai.device.type"])
873+
self.assertEqual(envelope.tags.get("ai.internal.sdkVersion"), azure_monitor_context["ai.internal.sdkVersion"])
874+
self.assertEqual(envelope.tags.get("ai.operation.id"), "{:032x}".format(span.context.trace_id))
875+
self.assertEqual(envelope.tags.get("ai.operation.parentId"), "{:016x}".format(span.context.span_id))
876+
self.assertEqual(envelope.time, "2019-12-04T21:18:36.027613Z")
877+
self.assertEqual(len(envelope.data.base_data.properties), 1)
878+
self.assertEqual(envelope.data.base_data.properties["test"], "asd")
879+
self.assertEqual(envelope.data.base_data.message, "test event")
880+
self.assertEqual(envelope.data.base_type, "MessageData")
881+
778882

779883
class TestAzureTraceExporterUtils(unittest.TestCase):
780884
def test_get_trace_export_result(self):

0 commit comments

Comments
 (0)