Skip to content

Commit cdd2c16

Browse files
authored
[exporter/azuremonitor] Add support for custom events in Azure Monitor exporter (#38609)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Add support for custom events in Azure Monitor exporter. Added a new config `custom_events_enabled ` (default = `false`): Enables export log record to custom events when there's attribute `microsoft.custom_event.name` or `APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE`. Since the `custom_events_enabled ` is false by default, it'll not break current behavior. The logic is align with current azure-sdk-for-js. https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/monitor/monitor-opentelemetry-exporter/src/utils/logUtils.ts#L78C3-L78C67 <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes #37422 <!--Describe what testing was performed and which tests were added.--> #### Testing - added new tests for custom event data - validated using multiple azure application insights, with enable or not enable `custom_events_enabled `. <!--Describe the documentation added.--> #### Documentation added new optional configuration ``` - `custom_events_enabled ` (default = `false`): Enables export log record to custom events when there's attribute `microsoft.custom_event.name` or `APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE`. ``` added document for custom events ``` #### Custom Events 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`. ``` <!--Please delete paragraphs that you did not use before submitting.-->
1 parent a9d9b3d commit cdd2c16

File tree

9 files changed

+208
-35
lines changed

9 files changed

+208
-35
lines changed

.chloggen/37422.yaml

+27
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: azuremonitorexporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: support custom event for logs for azure monitor 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: [37422]
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/azureblobexporter/README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ The following settings can be optionally configured and have default values:
3030
- metrics (default `metrics`): container to store metrics. default value is `metrics`.
3131
- logs (default `logs`): container to store logs. default value is `logs`.
3232
- traces (default `traces`): container to store traces. default value is `traces`.
33-
- blob_name_format:
34-
- metrics_format (default `2006/01/02/metrics_15_04_05_{{.SerialNum}}.{{.FileExtension}}`): blob name format. The date format follows constants in Golang, refer [here](https://go.dev/src/time/format.go).
35-
- logs_format (default `2006/01/02/logs_15_04_05_{{.SerialNum}}.{{.FileExtension}}`): blob name format.
36-
- traces_format (default `2006/01/02/traces_15_04_05_{{.SerialNum}}.{{.FileExtension}}`): blob name format.
37-
- serial_num_range (default `10000`): a range of random number for SerialNum.
33+
- blob_name_format: the final blob name will be blob_name +
34+
- metrics_format (default `2006/01/02/metrics_15_04_05.json`): blob name format. The date format follows constants in Golang, refer [here](https://go.dev/src/time/format.go).
35+
- logs_format (default `2006/01/02/logs_15_04_05.json`): blob name format.
36+
- traces_format (default `2006/01/02/traces_15_04_05.json`): blob name format.
37+
- serial_num_range (default `10000`): a range of random number to be appended after blob_name. e.g. `blob_name_{serial_num}`.
3838
- format (default `json`): `json` or `proto`. which present otel json or otel protobuf format, the file extension will be `json` or `pb`.
3939
- encodings (default using encoding specified in `format`, which is `json`): if specified, uses the encoding extension to encode telemetry data. Overrides format.
4040
- logs (default `nil`): encoding component id.

exporter/azuremonitorexporter/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ The following settings can be optionally configured:
4343
- `queue_size` (default = 1000): Maximum number of batches kept in memory before data; ignored if `enabled` is `false`
4444
- `storage` (default = `none`): When set, enables persistence and uses the component specified as a storage extension for the persistent queue
4545
- `shutdown_timeout` (default = 1s): Timeout to wait for graceful shutdown. Once exceeded, the component will shut down forcibly, dropping any element in queue.
46+
- `custom_events_enabled` (default = `false`): Enables export log record to custom events when there's attribute `microsoft.custom_event.name` or `APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE`.
4647

4748
Example:
4849

@@ -118,6 +119,10 @@ Exception events are saved to the Application Insights `exception` table.
118119
This exporter saves log records to Application Insights `traces` table.
119120
[TraceId](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-traceid) is mapped to `operation_id` column and [SpanId](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-spanid) is mapped to `operation_parentId` column.
120121

122+
#### Custom Events
123+
124+
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`.
125+
121126
### Metrics
122127

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

exporter/azuremonitorexporter/azuremonitor_exporter.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (exporter *azureMonitorExporter) Shutdown(_ context.Context) (err error) {
5353

5454
func (exporter *azureMonitorExporter) consumeLogs(_ context.Context, logData plog.Logs) error {
5555
resourceLogs := logData.ResourceLogs()
56-
logPacker := newLogPacker(exporter.logger)
56+
logPacker := newLogPacker(exporter.logger, exporter.config)
5757

5858
for i := 0; i < resourceLogs.Len(); i++ {
5959
scopeLogs := resourceLogs.At(i).ScopeLogs()

exporter/azuremonitorexporter/config.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import (
1212

1313
// Config defines configuration for Azure Monitor
1414
type Config struct {
15-
QueueSettings exporterhelper.QueueConfig `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"`
15+
QueueSettings exporterhelper.QueueConfig `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"`
2324
}

exporter/azuremonitorexporter/conventions.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import (
1616

1717
const (
1818
// TODO replace with convention.* values once/if available
19-
attributeOtelStatusCode string = "otel.status_code"
20-
attributeOtelStatusDescription string = "otel.status_description"
19+
attributeOtelStatusCode string = "otel.status_code"
20+
attributeOtelStatusDescription string = "otel.status_description"
21+
attributeMicrosoftCustomEventName string = "microsoft.custom_event.name"
22+
attributeApplicationInsightsEventMarkerAttribute string = "APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE"
2123
)
2224

2325
// NetworkAttributes is the set of known network attributes

exporter/azuremonitorexporter/factory.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ type factory struct {
4646

4747
func createDefaultConfig() component.Config {
4848
return &Config{
49-
MaxBatchSize: 1024,
50-
MaxBatchInterval: 10 * time.Second,
51-
SpanEventsEnabled: false,
52-
QueueSettings: exporterhelper.NewDefaultQueueConfig(),
53-
ShutdownTimeout: 1 * time.Second,
49+
MaxBatchSize: 1024,
50+
MaxBatchInterval: 10 * time.Second,
51+
SpanEventsEnabled: false,
52+
QueueSettings: exporterhelper.NewDefaultQueueConfig(),
53+
ShutdownTimeout: 1 * time.Second,
54+
CustomEventsEnabled: false,
5455
}
5556
}
5657

exporter/azuremonitorexporter/log_to_envelope.go

+60-9
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,48 @@ import (
1616

1717
type logPacker struct {
1818
logger *zap.Logger
19+
config *Config
1920
}
2021

21-
func (packer *logPacker) LogRecordToEnvelope(logRecord plog.LogRecord, resource pcommon.Resource, instrumentationScope pcommon.InstrumentationScope) *contracts.Envelope {
22+
func (packer *logPacker) initEnvelope(logRecord plog.LogRecord) (*contracts.Envelope, *contracts.Data) {
2223
envelope := contracts.NewEnvelope()
2324
envelope.Tags = make(map[string]string)
2425
envelope.Time = toTime(timestampFromLogRecord(logRecord)).Format(time.RFC3339Nano)
26+
return envelope, contracts.NewData()
27+
}
28+
29+
func (packer *logPacker) handleEventData(envelope *contracts.Envelope, data *contracts.Data, logRecord plog.LogRecord) {
30+
attributes := logRecord.Attributes()
31+
eventData := contracts.NewEventData()
32+
if val, ok := attributes.Get(attributeMicrosoftCustomEventName); ok {
33+
eventData.Name = val.AsString()
34+
} else if val, ok := attributes.Get(attributeApplicationInsightsEventMarkerAttribute); ok {
35+
eventData.Name = val.AsString()
36+
}
2537

26-
data := contracts.NewData()
38+
eventData.Properties = make(map[string]string)
39+
setAttributesAsProperties(attributes, eventData.Properties)
2740

41+
data.BaseData = eventData
42+
data.BaseType = eventData.BaseType()
43+
envelope.Name = eventData.EnvelopeName("")
44+
envelope.Data = data
45+
46+
packer.sanitizeAll(envelope, eventData)
47+
}
48+
49+
func (packer *logPacker) handleMessageData(envelope *contracts.Envelope, data *contracts.Data, logRecord plog.LogRecord, resource pcommon.Resource, instrumentationScope pcommon.InstrumentationScope) {
2850
messageData := contracts.NewMessageData()
2951
messageData.Properties = make(map[string]string)
30-
3152
messageData.SeverityLevel = packer.toAiSeverityLevel(logRecord.SeverityNumber())
32-
3353
messageData.Message = logRecord.Body().AsString()
3454

3555
envelope.Tags[contracts.OperationId] = traceutil.TraceIDToHexOrEmptyString(logRecord.TraceID())
3656
envelope.Tags[contracts.OperationParentId] = traceutil.SpanIDToHexOrEmptyString(logRecord.SpanID())
3757

38-
envelope.Name = messageData.EnvelopeName("")
39-
4058
data.BaseData = messageData
4159
data.BaseType = messageData.BaseType()
60+
envelope.Name = messageData.EnvelopeName("")
4261
envelope.Data = data
4362

4463
resourceAttributes := resource.Attributes()
@@ -49,9 +68,26 @@ func (packer *logPacker) LogRecordToEnvelope(logRecord plog.LogRecord, resource
4968

5069
setAttributesAsProperties(logRecord.Attributes(), messageData.Properties)
5170

52-
packer.sanitize(func() []string { return messageData.Sanitize() })
53-
packer.sanitize(func() []string { return envelope.Sanitize() })
71+
packer.sanitizeAll(envelope, messageData)
72+
}
73+
74+
func (packer *logPacker) sanitizeAll(envelope *contracts.Envelope, data any) {
75+
if sanitizer, ok := data.(interface{ Sanitize() []string }); ok {
76+
packer.sanitize(sanitizer.Sanitize)
77+
}
78+
packer.sanitize(envelope.Sanitize)
5479
packer.sanitize(func() []string { return contracts.SanitizeTags(envelope.Tags) })
80+
}
81+
82+
func (packer *logPacker) LogRecordToEnvelope(logRecord plog.LogRecord, resource pcommon.Resource, instrumentationScope pcommon.InstrumentationScope) *contracts.Envelope {
83+
envelope, data := packer.initEnvelope(logRecord)
84+
attributes := logRecord.Attributes()
85+
86+
if packer.config.CustomEventsEnabled && isEventData(attributes) {
87+
packer.handleEventData(envelope, data, logRecord)
88+
} else {
89+
packer.handleMessageData(envelope, data, logRecord, resource, instrumentationScope)
90+
}
5591

5692
return envelope
5793
}
@@ -79,9 +115,10 @@ func (packer *logPacker) toAiSeverityLevel(sn plog.SeverityNumber) contracts.Sev
79115
}
80116
}
81117

82-
func newLogPacker(logger *zap.Logger) *logPacker {
118+
func newLogPacker(logger *zap.Logger, config *Config) *logPacker {
83119
packer := &logPacker{
84120
logger: logger,
121+
config: config,
85122
}
86123
return packer
87124
}
@@ -97,3 +134,17 @@ func timestampFromLogRecord(lr plog.LogRecord) pcommon.Timestamp {
97134

98135
return pcommon.NewTimestampFromTime(timeNow())
99136
}
137+
138+
func hasOneOfKeys(attrMap pcommon.Map, keys ...string) bool {
139+
for _, key := range keys {
140+
_, exists := attrMap.Get(key)
141+
if exists {
142+
return true
143+
}
144+
}
145+
return false
146+
}
147+
148+
func isEventData(attrMap pcommon.Map) bool {
149+
return hasOneOfKeys(attrMap, attributeMicrosoftCustomEventName, attributeApplicationInsightsEventMarkerAttribute)
150+
}

exporter/azuremonitorexporter/logexporter_test.go

+91-5
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import (
2424

2525
const (
2626
defaultEnvelopeName = "Microsoft.ApplicationInsights.Message"
27-
defaultdBaseType = "MessageData"
27+
defaultBaseType = "MessageData"
28+
eventBaseType = "EventData"
2829
)
2930

3031
var (
@@ -54,8 +55,9 @@ func TestLogRecordToEnvelope(t *testing.T) {
5455
}
5556

5657
tests := []struct {
57-
name string
58-
index int
58+
name string
59+
baseType string
60+
index int
5961
}{
6062
{
6163
name: "timestamp is correct",
@@ -86,7 +88,7 @@ func TestLogRecordToEnvelope(t *testing.T) {
8688
assert.Equal(t, toTime(timestampFromLogRecord(logRecord)).Format(time.RFC3339Nano), envelope.Time)
8789
require.NotNil(t, envelope.Data)
8890
envelopeData := envelope.Data.(*contracts.Data)
89-
assert.Equal(t, defaultdBaseType, envelopeData.BaseType)
91+
assert.Equal(t, defaultBaseType, envelopeData.BaseType)
9092

9193
require.NotNil(t, envelopeData.BaseData)
9294

@@ -189,7 +191,7 @@ func getLogsExporter(config *Config, transportChannel appinsights.TelemetryChann
189191
}
190192

191193
func getLogPacker() *logPacker {
192-
return newLogPacker(zap.NewNop())
194+
return newLogPacker(zap.NewNop(), defaultConfig)
193195
}
194196

195197
func getTestLogs() plog.Logs {
@@ -261,3 +263,87 @@ func getTestLogRecord(index int) (pcommon.Resource, pcommon.InstrumentationScope
261263

262264
return resource, scope, logRecord
263265
}
266+
267+
func TestHandleEventData(t *testing.T) {
268+
logger := zap.NewNop()
269+
config := &Config{}
270+
packer := newLogPacker(logger, config)
271+
272+
tests := []struct {
273+
name string
274+
logRecord func() plog.LogRecord
275+
expectedEventName string
276+
expectedProperty map[string]string
277+
}{
278+
{
279+
name: "Event name from attributeMicrosoftCustomEventName",
280+
logRecord: func() plog.LogRecord {
281+
lr := plog.NewLogRecord()
282+
lr.Attributes().PutStr(attributeMicrosoftCustomEventName, "CustomEvent")
283+
lr.Attributes().PutStr("test_attribute", "test_value")
284+
return lr
285+
},
286+
expectedEventName: "CustomEvent",
287+
expectedProperty: map[string]string{
288+
attributeMicrosoftCustomEventName: "CustomEvent",
289+
"test_attribute": "test_value",
290+
},
291+
},
292+
{
293+
name: "Event name from attributeApplicationInsightsEventMarkerAttribute",
294+
logRecord: func() plog.LogRecord {
295+
lr := plog.NewLogRecord()
296+
lr.Attributes().PutStr(attributeApplicationInsightsEventMarkerAttribute, "MarkerEvent")
297+
lr.Attributes().PutStr("test_attribute", "test_value")
298+
return lr
299+
},
300+
expectedEventName: "MarkerEvent",
301+
expectedProperty: map[string]string{
302+
attributeApplicationInsightsEventMarkerAttribute: "MarkerEvent",
303+
"test_attribute": "test_value",
304+
},
305+
},
306+
{
307+
name: "No event name attributes",
308+
logRecord: func() plog.LogRecord {
309+
lr := plog.NewLogRecord()
310+
lr.Attributes().PutStr("test_attribute", "test_value")
311+
return lr
312+
},
313+
expectedEventName: "",
314+
expectedProperty: map[string]string{
315+
"test_attribute": "test_value",
316+
},
317+
},
318+
}
319+
320+
for _, tt := range tests {
321+
t.Run(tt.name, func(t *testing.T) {
322+
envelope := contracts.NewEnvelope()
323+
data := contracts.NewData()
324+
logRecord := tt.logRecord()
325+
326+
packer.handleEventData(envelope, data, logRecord)
327+
328+
eventData := data.BaseData.(*contracts.EventData)
329+
assert.Equal(t, tt.expectedEventName, eventData.Name)
330+
assert.Equal(t, tt.expectedProperty, eventData.Properties)
331+
})
332+
}
333+
}
334+
335+
func TestSetAttributesAsProperties(t *testing.T) {
336+
properties := make(map[string]string)
337+
attributes := pcommon.NewMap()
338+
attributes.PutStr("string_key", "string_value")
339+
attributes.PutInt("int_key", 123)
340+
attributes.PutDouble("double_key", 4.56)
341+
attributes.PutBool("bool_key", true)
342+
343+
setAttributesAsProperties(attributes, properties)
344+
345+
assert.Equal(t, "string_value", properties["string_key"])
346+
assert.Equal(t, "123", properties["int_key"])
347+
assert.Equal(t, "4.56", properties["double_key"])
348+
assert.Equal(t, "true", properties["bool_key"])
349+
}

0 commit comments

Comments
 (0)