diff --git a/collector/receiver/telemetryapireceiver/README.md b/collector/receiver/telemetryapireceiver/README.md index 70a71cf9a9..da86793fe4 100644 --- a/collector/receiver/telemetryapireceiver/README.md +++ b/collector/receiver/telemetryapireceiver/README.md @@ -13,6 +13,20 @@ Supported events: * `platform.initStart` - The receiver uses this event to record the start time of the function initialization period. Once both start and end times are recorded, the receiver generates a span named `platform.initRuntimeDone` to record the event. * `platform.initRuntimeDone` - The receiver uses this event to record the end time of the function initialization period. Once both start and end times are recorded, the receiver generates a span named `platform.initRuntimeDone` to record the event. +## Logs metadata reserved fields + +The following field names are reserved for internal use in logs metadata and must not be used as custom metadata keys: + +| Field | Description | +|-------------|------------------------------------------| +| `level` | Log severity level | +| `message` | Log message body | +| `requestId` | AWS Lambda request identifier | +| `timestamp` | Time of the log event | +| `type` | Telemetry API event type | + +> **Note:** These fields are populated internally by the receiver and will take priority over any user-provided metadata with the same name. You should be aware of these limitations and handle conflicts at the business logic level — for example, by renaming custom fields that collide with reserved names before they are emitted as log metadata. + ## Configuration | Field | Default | Description | diff --git a/collector/receiver/telemetryapireceiver/receiver.go b/collector/receiver/telemetryapireceiver/receiver.go index 717244b431..5ce5025e73 100644 --- a/collector/receiver/telemetryapireceiver/receiver.go +++ b/collector/receiver/telemetryapireceiver/receiver.go @@ -490,6 +490,20 @@ func (r *telemetryAPIReceiver) createLogs(slice []event) (plog.Logs, error) { } } else if line, ok := record["message"].(string); ok { logRecord.Body().SetStr(line) + + for key, value := range record { + switch key { + case "level", "message", "requestId", "timestamp", "type": + continue + default: + attr, _ := logRecord.Attributes().GetOrPutEmpty(key) + if err := attr.FromRaw(value); err != nil { + logRecord.Attributes().Remove(key) + r.logger.Warn("Failed while converting field to attribute", zap.String("key", key), zap.Error(err)) + continue + } + } + } } } else { if requestId := r.getCurrentRequestId(); requestId != "" { diff --git a/collector/receiver/telemetryapireceiver/receiver_test.go b/collector/receiver/telemetryapireceiver/receiver_test.go index 476f22184f..7e549a628d 100644 --- a/collector/receiver/telemetryapireceiver/receiver_test.go +++ b/collector/receiver/telemetryapireceiver/receiver_test.go @@ -359,6 +359,7 @@ func TestCreateLogs(t *testing.T) { containsRequestId bool requestId string severityNumber plog.SeverityNumber + attributes map[string]interface{} } testCases := []struct { @@ -479,6 +480,137 @@ func TestCreateLogs(t *testing.T) { }, expectError: false, }, + { + desc: "function json with extra fields", + slice: []event{ + { + Time: "2026-02-26T20:15:32.000Z", + Type: "function", + Record: map[string]any{ + "timestamp": "2026-02-26T20:15:32.000Z", + "level": "INFO", + "requestId": "79b4f56e-95b1-4643-9700-2807f4e6", + "message": "Hello world, I am a function with extra data!", + "extraString": "stringValue", + "extraNumber": float64(2217), + "extraFloat": 3.14, + "extraBoolean": true, + "extraNull": nil, + "extraArrayOfStrings": []any{"stringValue", "stringValue"}, + "extraArrayOfNumbers": []any{float64(2217), float64(2217)}, + "extraArrayOfMixedTypes": []any{"stringValue", float64(2217), true, nil}, + "extraArrayWithNesting": []any{"stringValue", []any{float64(2217), []any{true, nil}}}, + "extraObject": map[string]any{ + "stringValue": "stringValue", + "numberValue": float64(2217), + "booleanValue": true, + "nullValue": nil, + }, + "extraObjectWithNesting": map[string]any{ + "stringValue": "stringValue", + "numberValue": float64(2217), + "booleanValue": true, + "nullValue": nil, + "arrayValue": []any{"stringValue", float64(2217)}, + "objectValue": map[string]any{ + "stringValue": "stringValue", + "numberValue": float64(2217), + }, + }, + }, + }, + }, + expectedLogs: []logInfo{ + { + logType: "function", + timestamp: "2026-02-26T20:15:32.000Z", + body: "Hello world, I am a function with extra data!", + containsRequestId: true, + requestId: "79b4f56e-95b1-4643-9700-2807f4e6", + severityText: "Info", + severityNumber: plog.SeverityNumberInfo, + attributes: map[string]any{ + "extraString": "stringValue", + "extraNumber": float64(2217), + "extraFloat": float64(3.14), + "extraBoolean": true, + "extraNull": nil, + "extraArrayOfStrings": []any{ + "stringValue", + "stringValue", + }, + "extraArrayOfNumbers": []any{ + float64(2217), + float64(2217), + }, + "extraArrayOfMixedTypes": []any{ + "stringValue", + float64(2217), + true, + nil, + }, + "extraArrayWithNesting": []any{ + "stringValue", + []any{ + float64(2217), + []any{ + true, nil, + }, + }, + }, + "extraObject": map[string]any{ + "stringValue": "stringValue", + "numberValue": float64(2217), + "booleanValue": true, + "nullValue": nil, + }, + "extraObjectWithNesting": map[string]any{ + "stringValue": "stringValue", + "numberValue": float64(2217), + "booleanValue": true, + "nullValue": nil, + "arrayValue": []any{ + "stringValue", + float64(2217), + }, + "objectValue": map[string]any{ + "stringValue": "stringValue", + "numberValue": float64(2217), + }, + }, + }, + }, + }, + expectError: false, + }, + { + desc: "function json with keeping reserved fields intact", + slice: []event{ + { + Time: "2026-02-26T20:15:32.000Z", + Type: "function", + Record: map[string]any{ + "timestamp": "2026-02-26T20:15:32.000Z", + "level": "INFO", + "requestId": "79b4f56e-95b1-4643-9700-2807f4e6", + "message": "Hello world, I am a function with overriden type field!", + "type": "override", + }, + }, + }, + expectedLogs: []logInfo{ + { + logType: "function", + timestamp: "2026-02-26T20:15:32.000Z", + body: "Hello world, I am a function with overriden type field!", + containsRequestId: true, + requestId: "79b4f56e-95b1-4643-9700-2807f4e6", + severityText: "Info", + severityNumber: plog.SeverityNumberInfo, + }, + }, + expectError: false, + }, { desc: "extension text", slice: []event{ @@ -820,6 +952,13 @@ func TestCreateLogs(t *testing.T) { require.Equal(t, expected.severityText, logRecord.SeverityText()) require.Equal(t, expected.severityNumber, logRecord.SeverityNumber()) require.Equal(t, expected.body, logRecord.Body().Str()) + + // Check expected attributes + for key, value := range expected.attributes { + attr, ok := logRecord.Attributes().Get(key) + require.True(t, ok, "expected attribute %s not found", key) + require.Equal(t, value, attr.AsRaw(), "expected attribute %s to have value %v, got %v", key, value, attr.AsRaw()) + } } }) }