Skip to content

Commit 5445b8e

Browse files
rafaelrodrigues3092MikeGoldsmithTylerHelmuth
authored
[pkg/translator/azurelogs] Support additional fields not in the common schema (#46165)
#### Description Azure resource logs are not standard, see [Common and service-specific schemas for Azure resource logs](https://learn.microsoft.com/en-us/azure/azure-monitor/platform/resource-logs-schema). When consuming resource logs for services like Service Bus or Event Hub, timestamp parsing fails. The log events are also not helpful identifying the resource or resource type causing the parsing issue. This change: - Adds support for alternative timestamp fields: `EventTimeString`, `EventTimestamp`, `startTime` - Adds alternative field for properties: `EventProperties` - Improves warning logs to be able to better identify the resource and resource type that caused the parsing issue. #### Link to tracking issue Fixes #46162 #### Testing - Added testing for the added fields. - Tested two scenarios - Previously supported payload - Previously unsupported payload #### Documentation Updated Azure Event Hub Receiver ReadMe to include the additional supported fields. #### Additional Notes Assisted-by: Claude Opus 4.6 --------- Co-authored-by: Mike Goldsmith <goldsmith.mike@gmail.com> Co-authored-by: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com>
1 parent d3d1efb commit 5445b8e

4 files changed

Lines changed: 325 additions & 28 deletions

File tree

.chloggen/46162.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
7+
component: pkg/azurelogs
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: 'Add support for additional time and property fields in Azure Resource Logs'
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: [46162]
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+
Azure resource logs are not standard. This change expands support for different resource types by adding support
20+
for additional time and property fields that are less commonly used in Azure resource logs.
21+
22+
# If your change doesn't affect end users or the exported elements of any package,
23+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
24+
# Optional: The change log or logs in which this entry should be included.
25+
# e.g. '[user]' or '[user, api]'
26+
# Include 'user' if the change is relevant to end users.
27+
# Include 'api' if there is a change to a library API.
28+
# Default: '[user]'
29+
change_logs: []

pkg/translator/azurelogs/resourcelogs_to_logs.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,16 @@ type identity struct {
103103
// azureLogRecord represents a single Azure log following
104104
// the common schema:
105105
// https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-schema
106+
// Some services don't follow the common schema and may have different fields, but these are the most common ones across all services and are used for grouping and semantic conventions mapping.
107+
// Example:
108+
// - Service Bus: https://learn.microsoft.com/en-us/azure/service-bus-messaging/monitor-service-bus-reference#operational-logs
109+
// - Event Hub: https://learn.microsoft.com/en-us/azure/event-hubs/monitor-event-hubs-reference#archive-logs-schema
106110
type azureLogRecord struct {
107111
Time string `json:"time"`
108112
Timestamp string `json:"timeStamp"`
113+
EventTimeString string `json:"EventTimeString"`
114+
EventTimestamp string `json:"EventTimestamp"`
115+
StartTime string `json:"startTime"`
109116
ResourceID string `json:"resourceId"`
110117
TenantID *string `json:"tenantId"`
111118
OperationName string `json:"operationName"`
@@ -121,6 +128,16 @@ type azureLogRecord struct {
121128
Level *json.Number `json:"Level"`
122129
Location *string `json:"location"`
123130
Properties json.RawMessage `json:"properties"`
131+
EventProperties json.RawMessage `json:"EventProperties"`
132+
}
133+
134+
// getProperties returns whichever properties field is populated,
135+
// preferring "properties" over "EventProperties".
136+
func (r *azureLogRecord) getProperties() json.RawMessage {
137+
if len(r.Properties) > 0 {
138+
return r.Properties
139+
}
140+
return r.EventProperties
124141
}
125142

126143
var _ plog.Unmarshaler = (*ResourceLogsUnmarshaler)(nil)
@@ -182,7 +199,14 @@ func (r ResourceLogsUnmarshaler) UnmarshalLogs(buf []byte) (plog.Logs, error) {
182199

183200
nanos, err := getTimestamp(log, r.TimeFormats...)
184201
if err != nil {
185-
r.Logger.Warn("Unable to convert timestamp from log", zap.String("timestamp", log.Time))
202+
fields := []zap.Field{zap.String("timestamp", log.Time)}
203+
if log.ResourceID != "" {
204+
fields = append(fields, zap.String("resource_id", log.ResourceID))
205+
}
206+
if log.Category != "" {
207+
fields = append(fields, zap.String("category", log.Category))
208+
}
209+
r.Logger.Warn("Unable to convert timestamp from log", fields...)
186210
continue
187211
}
188212

@@ -199,7 +223,7 @@ func (r ResourceLogsUnmarshaler) UnmarshalLogs(buf []byte) (plog.Logs, error) {
199223
lr.SetSeverityText(log.Level.String())
200224
}
201225

202-
err = addRecordAttributes(log.Category, log.Properties, lr)
226+
err = addRecordAttributes(log.Category, log.getProperties(), lr)
203227
if err != nil {
204228
if errors.Is(err, errStillToImplement) || errors.Is(err, errUnsupportedCategory) {
205229
// TODO @constanca-m This will be removed once the categories
@@ -247,13 +271,20 @@ func (r ResourceLogsUnmarshaler) UnmarshalLogs(buf []byte) (plog.Logs, error) {
247271
}
248272

249273
func getTimestamp(record *azureLogRecord, formats ...string) (pcommon.Timestamp, error) {
250-
if record.Time != "" {
274+
switch {
275+
case record.Time != "":
251276
return asTimestamp(record.Time, formats...)
252-
} else if record.Timestamp != "" {
277+
case record.Timestamp != "":
253278
return asTimestamp(record.Timestamp, formats...)
279+
case record.EventTimeString != "":
280+
return asTimestamp(record.EventTimeString, formats...)
281+
case record.EventTimestamp != "":
282+
return asTimestamp(record.EventTimestamp, formats...)
283+
case record.StartTime != "":
284+
return asTimestamp(record.StartTime, formats...)
285+
default:
286+
return 0, errMissingTimestamp
254287
}
255-
256-
return 0, errMissingTimestamp
257288
}
258289

259290
// asTimestamp will parse an ISO8601 string into an OpenTelemetry
@@ -357,12 +388,13 @@ func extractRawAttributes(log *azureLogRecord, rawRecord json.RawMessage) map[st
357388
attrs[azureOperationName] = log.OperationName
358389
setIf(attrs, azureOperationVersion, log.OperationVersion)
359390

360-
if log.Properties != nil {
361-
copyPropertiesAndApplySemanticConventions(log.Category, log.Properties, attrs)
391+
properties := log.getProperties()
392+
if properties != nil {
393+
copyPropertiesAndApplySemanticConventions(log.Category, properties, attrs)
362394
}
363395

364396
// The original log needs to be preserved for logs that don't have a properties field
365-
if len(log.Properties) == 0 && len(rawRecord) > 0 {
397+
if len(properties) == 0 && len(rawRecord) > 0 {
366398
// Format the JSON with proper indentation to match expected output
367399
var formattedJSON bytes.Buffer
368400
if err := json.Indent(&formattedJSON, rawRecord, "", "\t"); err == nil {

pkg/translator/azurelogs/resourcelogs_to_logs_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,3 +942,239 @@ func TestUnmarshalLogs_StableGates(t *testing.T) {
942942
})
943943
}
944944
}
945+
946+
func TestGetProperties(t *testing.T) {
947+
tests := []struct {
948+
name string
949+
properties json.RawMessage
950+
eventProperties json.RawMessage
951+
expected json.RawMessage
952+
}{
953+
{
954+
name: "properties set, eventProperties empty",
955+
properties: json.RawMessage(`{"key":"value"}`),
956+
eventProperties: nil,
957+
expected: json.RawMessage(`{"key":"value"}`),
958+
},
959+
{
960+
name: "properties empty, eventProperties set",
961+
properties: nil,
962+
eventProperties: json.RawMessage(`{"event":"data"}`),
963+
expected: json.RawMessage(`{"event":"data"}`),
964+
},
965+
{
966+
name: "both set, properties takes precedence",
967+
properties: json.RawMessage(`{"key":"value"}`),
968+
eventProperties: json.RawMessage(`{"event":"data"}`),
969+
expected: json.RawMessage(`{"key":"value"}`),
970+
},
971+
{
972+
name: "both empty",
973+
properties: nil,
974+
eventProperties: nil,
975+
expected: nil,
976+
},
977+
}
978+
979+
for _, tt := range tests {
980+
t.Run(tt.name, func(t *testing.T) {
981+
record := &azureLogRecord{
982+
Properties: tt.properties,
983+
EventProperties: tt.eventProperties,
984+
}
985+
result := record.getProperties()
986+
assert.Equal(t, string(tt.expected), string(result))
987+
})
988+
}
989+
}
990+
991+
func TestGetTimestamp(t *testing.T) {
992+
tests := []struct {
993+
name string
994+
record *azureLogRecord
995+
formats []string
996+
expectError bool
997+
}{
998+
{
999+
name: "time field set",
1000+
record: &azureLogRecord{
1001+
Time: "2022-11-11T04:48:27.6767145Z",
1002+
},
1003+
expectError: false,
1004+
},
1005+
{
1006+
name: "timestamp field set",
1007+
record: &azureLogRecord{
1008+
Timestamp: "2022-11-11T04:48:27.6767145Z",
1009+
},
1010+
expectError: false,
1011+
},
1012+
{
1013+
name: "EventTimeString field set",
1014+
record: &azureLogRecord{
1015+
EventTimeString: "2022-11-11T04:48:27.6767145Z",
1016+
},
1017+
expectError: false,
1018+
},
1019+
{
1020+
name: "time takes precedence over timestamp",
1021+
record: &azureLogRecord{
1022+
Time: "2022-11-11T04:48:27.6767145Z",
1023+
Timestamp: "2023-01-01T00:00:00Z",
1024+
},
1025+
expectError: false,
1026+
},
1027+
{
1028+
name: "timestamp takes precedence over EventTimeString",
1029+
record: &azureLogRecord{
1030+
Timestamp: "2022-11-11T04:48:27.6767145Z",
1031+
EventTimeString: "2023-01-01T00:00:00Z",
1032+
},
1033+
expectError: false,
1034+
},
1035+
{
1036+
name: "all empty returns error",
1037+
record: &azureLogRecord{},
1038+
expectError: true,
1039+
},
1040+
{
1041+
name: "EventTimeString with custom format",
1042+
record: &azureLogRecord{
1043+
EventTimeString: "11/20/2024 13:57:18",
1044+
},
1045+
formats: []string{"01/02/2006 15:04:05"},
1046+
expectError: false,
1047+
},
1048+
{
1049+
name: "startTime field set",
1050+
record: &azureLogRecord{
1051+
StartTime: "2022-11-11T04:48:27.6767145Z",
1052+
},
1053+
expectError: false,
1054+
},
1055+
{
1056+
name: "EventTimeString takes precedence over startTime",
1057+
record: &azureLogRecord{
1058+
EventTimeString: "2022-11-11T04:48:27.6767145Z",
1059+
StartTime: "2023-01-01T00:00:00Z",
1060+
},
1061+
expectError: false,
1062+
},
1063+
{
1064+
name: "EventTimestamp field set",
1065+
record: &azureLogRecord{
1066+
EventTimestamp: "2022-11-11T04:48:27.6767145Z",
1067+
},
1068+
expectError: false,
1069+
},
1070+
{
1071+
name: "EventTimestamp with custom format",
1072+
record: &azureLogRecord{
1073+
EventTimestamp: "11/20/2024 13:57:18",
1074+
},
1075+
formats: []string{"01/02/2006 15:04:05"},
1076+
expectError: false,
1077+
},
1078+
{
1079+
name: "EventTimeString takes precedence over EventTimestamp",
1080+
record: &azureLogRecord{
1081+
EventTimeString: "2022-11-11T04:48:27.6767145Z",
1082+
EventTimestamp: "2023-01-01T00:00:00Z",
1083+
},
1084+
expectError: false,
1085+
},
1086+
{
1087+
name: "EventTimestamp takes precedence over startTime",
1088+
record: &azureLogRecord{
1089+
EventTimestamp: "2022-11-11T04:48:27.6767145Z",
1090+
StartTime: "2023-01-01T00:00:00Z",
1091+
},
1092+
expectError: false,
1093+
},
1094+
{
1095+
name: "startTime with custom format",
1096+
record: &azureLogRecord{
1097+
StartTime: "11/20/2024 13:57:18",
1098+
},
1099+
formats: []string{"01/02/2006 15:04:05"},
1100+
expectError: false,
1101+
},
1102+
}
1103+
1104+
for _, tt := range tests {
1105+
t.Run(tt.name, func(t *testing.T) {
1106+
nanos, err := getTimestamp(tt.record, tt.formats...)
1107+
if tt.expectError {
1108+
assert.Error(t, err)
1109+
assert.Equal(t, pcommon.Timestamp(0), nanos)
1110+
} else {
1111+
assert.NoError(t, err)
1112+
assert.Less(t, pcommon.Timestamp(0), nanos)
1113+
}
1114+
})
1115+
}
1116+
}
1117+
1118+
func TestExtractRawAttributes_EventProperties(t *testing.T) {
1119+
tests := []struct {
1120+
name string
1121+
log *azureLogRecord
1122+
expected map[string]any
1123+
}{
1124+
{
1125+
name: "uses EventProperties when Properties is nil",
1126+
log: &azureLogRecord{
1127+
OperationName: "operation.name",
1128+
Category: "category",
1129+
EventProperties: json.RawMessage(`{"a":1,"b":"two"}`),
1130+
},
1131+
expected: map[string]any{
1132+
azureOperationName: "operation.name",
1133+
azureCategory: "category",
1134+
azureProperties: map[string]any{
1135+
"a": float64(1),
1136+
"b": "two",
1137+
},
1138+
},
1139+
},
1140+
{
1141+
name: "Properties takes precedence over EventProperties",
1142+
log: &azureLogRecord{
1143+
OperationName: "operation.name",
1144+
Category: "category",
1145+
Properties: json.RawMessage(`{"from":"properties"}`),
1146+
EventProperties: json.RawMessage(`{"from":"eventproperties"}`),
1147+
},
1148+
expected: map[string]any{
1149+
azureOperationName: "operation.name",
1150+
azureCategory: "category",
1151+
azureProperties: map[string]any{
1152+
"from": "properties",
1153+
},
1154+
},
1155+
},
1156+
{
1157+
name: "rawRecord preserved when no properties",
1158+
log: &azureLogRecord{
1159+
OperationName: "operation.name",
1160+
Category: "category",
1161+
},
1162+
expected: map[string]any{
1163+
azureOperationName: "operation.name",
1164+
azureCategory: "category",
1165+
attributeEventOriginal: "{\n\t\"test\": true\n}",
1166+
},
1167+
},
1168+
}
1169+
1170+
for _, tt := range tests {
1171+
t.Run(tt.name, func(t *testing.T) {
1172+
var rawRecord json.RawMessage
1173+
if tt.name == "rawRecord preserved when no properties" {
1174+
rawRecord = json.RawMessage(`{"test": true}`)
1175+
}
1176+
result := extractRawAttributes(tt.log, rawRecord)
1177+
assert.Equal(t, tt.expected, result)
1178+
})
1179+
}
1180+
}

0 commit comments

Comments
 (0)