Skip to content

Commit d50c5cc

Browse files
authored
[exporter/azuremonitor] Support handling log exceptions (#39962)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Support handling exceptions to azure application insights `exceptions` table, and the logic aligned with implementation in azure SDK like [js](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/monitor/monitor-opentelemetry-exporter/src/utils/logUtils.ts#L78C3-L78C67), [python](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/logs/_exporter.py#L143). <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes #38432 . <!--Describe what testing was performed and which tests were added.--> #### Testing 1. added unit tests. 2. validated using 2 application insights. some results showed below. <img width="842" alt="image" src="https://github.com/user-attachments/assets/ce101ef1-00a2-47bf-8930-d51cdeb180cc" /> <!--Describe the documentation added.--> #### Documentation As described in README.md <!--Please delete paragraphs that you did not use before submitting.-->
1 parent 1564941 commit d50c5cc

File tree

5 files changed

+186
-11
lines changed

5 files changed

+186
-11
lines changed

.chloggen/38432.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: 'enhancement'
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: azuremonitorexxporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: support logging exceptions for azuremonitor exporter
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [38432]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

exporter/azuremonitorexporter/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ This exporter saves log records to Application Insights `traces` table.
126126

127127
When `custom_events_enabled` = `true`, azure monitor exporter will export log record to custom events when there's attribute `microsoft.custom_event.name` or `APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE`.
128128

129+
#### Exceptions
130+
131+
This exporter saves exception records to Application Insights `exceptions` table when log records indicate an excetion [specification](https://opentelemetry.io/docs/specs/otel/trace/exceptions/).
132+
133+
When `exception_event_enabled` = `true`, azure monitor exxporter will export log record to exceptions when there's attributes `exception.message` or `exception.type`.
134+
129135
### Metrics
130136

131137
This exporter saves metrics to Application Insights `customMetrics` table.

exporter/azuremonitorexporter/config.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import (
1212

1313
// Config defines configuration for Azure Monitor
1414
type Config struct {
15-
QueueSettings exporterhelper.QueueBatchConfig `mapstructure:"sending_queue"`
16-
Endpoint string `mapstructure:"endpoint"`
17-
ConnectionString configopaque.String `mapstructure:"connection_string"`
18-
InstrumentationKey configopaque.String `mapstructure:"instrumentation_key"`
19-
MaxBatchSize int `mapstructure:"maxbatchsize"`
20-
MaxBatchInterval time.Duration `mapstructure:"maxbatchinterval"`
21-
SpanEventsEnabled bool `mapstructure:"spaneventsenabled"`
22-
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
23-
CustomEventsEnabled bool `mapstructure:"custom_events_enabled"`
15+
QueueSettings exporterhelper.QueueBatchConfig `mapstructure:"sending_queue"`
16+
Endpoint string `mapstructure:"endpoint"`
17+
ConnectionString configopaque.String `mapstructure:"connection_string"`
18+
InstrumentationKey configopaque.String `mapstructure:"instrumentation_key"`
19+
MaxBatchSize int `mapstructure:"maxbatchsize"`
20+
MaxBatchInterval time.Duration `mapstructure:"maxbatchinterval"`
21+
SpanEventsEnabled bool `mapstructure:"spaneventsenabled"`
22+
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
23+
CustomEventsEnabled bool `mapstructure:"custom_events_enabled"`
24+
ExceptionEventsEnabled bool `mapstructure:"exception_events_enabled"`
2425
}

exporter/azuremonitorexporter/log_to_envelope.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/microsoft/ApplicationInsights-Go/appinsights/contracts"
1010
"go.opentelemetry.io/collector/pdata/pcommon"
1111
"go.opentelemetry.io/collector/pdata/plog"
12+
conventions "go.opentelemetry.io/otel/semconv/v1.27.0"
1213
"go.uber.org/zap"
1314

1415
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/traceutil"
@@ -83,15 +84,48 @@ func (packer *logPacker) LogRecordToEnvelope(logRecord plog.LogRecord, resource
8384
envelope, data := packer.initEnvelope(logRecord)
8485
attributes := logRecord.Attributes()
8586

86-
if packer.config.CustomEventsEnabled && isEventData(attributes) {
87+
switch {
88+
case packer.config.CustomEventsEnabled && isEventData(attributes):
8789
packer.handleEventData(envelope, data, logRecord)
88-
} else {
90+
case packer.config.ExceptionEventsEnabled && isExceptionData(attributes):
91+
packer.handleExceptionData(envelope, data, logRecord, resource, instrumentationScope)
92+
default:
8993
packer.handleMessageData(envelope, data, logRecord, resource, instrumentationScope)
9094
}
9195

9296
return envelope
9397
}
9498

99+
func (packer *logPacker) handleExceptionData(envelope *contracts.Envelope, data *contracts.Data, logRecord plog.LogRecord, resource pcommon.Resource, instrumentationScope pcommon.InstrumentationScope) {
100+
logAttributeMap := logRecord.Attributes()
101+
exceptionData := contracts.NewExceptionData()
102+
exceptionData.Properties = make(map[string]string)
103+
exceptionData.SeverityLevel = packer.toAiSeverityLevel(logRecord.SeverityNumber())
104+
exceptionData.ProblemId = logRecord.SeverityText()
105+
106+
exceptionDetails := mapIncomingAttributeMapExceptionDetail(logAttributeMap)
107+
exceptionData.Exceptions = append(exceptionData.Exceptions, exceptionDetails)
108+
109+
envelope.Name = exceptionData.EnvelopeName("")
110+
111+
data.BaseData = exceptionData
112+
data.BaseType = exceptionData.BaseType()
113+
envelope.Data = data
114+
115+
envelope.Tags[contracts.OperationId] = traceutil.TraceIDToHexOrEmptyString(logRecord.TraceID())
116+
envelope.Tags[contracts.OperationParentId] = traceutil.SpanIDToHexOrEmptyString(logRecord.SpanID())
117+
118+
resourceAttributes := resource.Attributes()
119+
applyResourcesToDataProperties(exceptionData.Properties, resourceAttributes)
120+
applyInstrumentationScopeValueToDataProperties(exceptionData.Properties, instrumentationScope)
121+
applyCloudTagsToEnvelope(envelope, resourceAttributes)
122+
applyInternalSdkVersionTagToEnvelope(envelope)
123+
124+
setAttributesAsProperties(logAttributeMap, exceptionData.Properties)
125+
126+
packer.sanitizeAll(envelope, exceptionData)
127+
}
128+
95129
func (packer *logPacker) sanitize(sanitizeFunc func() []string) {
96130
for _, warning := range sanitizeFunc() {
97131
packer.logger.Warn(warning)
@@ -148,3 +182,22 @@ func hasOneOfKeys(attrMap pcommon.Map, keys ...string) bool {
148182
func isEventData(attrMap pcommon.Map) bool {
149183
return hasOneOfKeys(attrMap, attributeMicrosoftCustomEventName, attributeApplicationInsightsEventMarkerAttribute)
150184
}
185+
186+
func isExceptionData(attributes pcommon.Map) bool {
187+
return hasOneOfKeys(attributes, string(conventions.ExceptionTypeKey), string(conventions.ExceptionMessageKey))
188+
}
189+
190+
func mapIncomingAttributeMapExceptionDetail(attributemap pcommon.Map) *contracts.ExceptionDetails {
191+
exceptionDetails := contracts.NewExceptionDetails()
192+
if message, exists := attributemap.Get(string(conventions.ExceptionMessageKey)); exists {
193+
exceptionDetails.Message = message.Str()
194+
}
195+
if typeName, exists := attributemap.Get(string(conventions.ExceptionTypeKey)); exists {
196+
exceptionDetails.TypeName = typeName.Str()
197+
}
198+
if stackTrace, exists := attributemap.Get(string(conventions.ExceptionStacktraceKey)); exists {
199+
exceptionDetails.HasFullStack = true
200+
exceptionDetails.Stack = stackTrace.Str()
201+
}
202+
return exceptionDetails
203+
}

exporter/azuremonitorexporter/logexporter_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,91 @@ func TestSetAttributesAsProperties(t *testing.T) {
347347
assert.Equal(t, "4.56", properties["double_key"])
348348
assert.Equal(t, "true", properties["bool_key"])
349349
}
350+
351+
func TestHandleExceptionDataWithDetails(t *testing.T) {
352+
logger := zap.NewNop()
353+
config := &Config{}
354+
packer := newLogPacker(logger, config)
355+
356+
tests := []struct {
357+
name string
358+
severityNum plog.SeverityNumber
359+
severityText string
360+
exceptionType string
361+
exceptionMessage string
362+
stackTrace string
363+
resourceAttrs map[string]any
364+
}{
365+
{
366+
name: "Full exception details",
367+
severityNum: plog.SeverityNumberError,
368+
severityText: "RuntimeError",
369+
exceptionType: "TypeError",
370+
exceptionMessage: "Cannot read property 'undefined'",
371+
stackTrace: "at Object.method (/path/file.js:10)\nat Object.method2 (/path/file2.js:20)",
372+
resourceAttrs: map[string]any{
373+
string(conventions.ServiceNameKey): "testService",
374+
"custom.attr": "value",
375+
},
376+
},
377+
{
378+
name: "Minimal exception details",
379+
severityNum: plog.SeverityNumberFatal,
380+
severityText: "FatalError",
381+
exceptionType: "SystemError",
382+
exceptionMessage: "System crash",
383+
resourceAttrs: map[string]any{},
384+
},
385+
}
386+
387+
for _, tt := range tests {
388+
t.Run(tt.name, func(t *testing.T) {
389+
envelope := contracts.NewEnvelope()
390+
envelope.Tags = make(map[string]string)
391+
data := contracts.NewData()
392+
393+
logRecord := plog.NewLogRecord()
394+
logRecord.SetSeverityNumber(tt.severityNum)
395+
logRecord.SetSeverityText(tt.severityText)
396+
397+
attrs := logRecord.Attributes()
398+
attrs.PutStr(string(conventions.ExceptionTypeKey), tt.exceptionType)
399+
attrs.PutStr(string(conventions.ExceptionMessageKey), tt.exceptionMessage)
400+
if tt.stackTrace != "" {
401+
attrs.PutStr(string(conventions.ExceptionStacktraceKey), tt.stackTrace)
402+
}
403+
404+
resource := pcommon.NewResource()
405+
for k, v := range tt.resourceAttrs {
406+
if str, ok := v.(string); ok {
407+
resource.Attributes().PutStr(k, str)
408+
}
409+
}
410+
411+
scope := pcommon.NewInstrumentationScope()
412+
413+
packer.handleExceptionData(envelope, data, logRecord, resource, scope)
414+
415+
exceptionData := data.BaseData.(*contracts.ExceptionData)
416+
assert.Equal(t, tt.severityText, exceptionData.ProblemId)
417+
assert.NotEmpty(t, exceptionData.Properties)
418+
419+
require.Len(t, exceptionData.Exceptions, 1)
420+
exception := exceptionData.Exceptions[0]
421+
assert.Equal(t, tt.exceptionType, exception.TypeName)
422+
assert.Equal(t, tt.exceptionMessage, exception.Message)
423+
424+
if tt.stackTrace != "" {
425+
assert.Equal(t, tt.stackTrace, exception.Stack)
426+
assert.True(t, exception.HasFullStack)
427+
}
428+
429+
// Resource attributes should be copied to properties
430+
for k, v := range tt.resourceAttrs {
431+
if str, ok := v.(string); ok {
432+
assert.Equal(t, str, exceptionData.Properties[k])
433+
}
434+
}
435+
})
436+
}
437+
}

0 commit comments

Comments
 (0)