Skip to content

Commit 4f7ee02

Browse files
committed
Implement JSON preprocessing for structured log message extraction and context serialization
1 parent 18165b8 commit 4f7ee02

4 files changed

Lines changed: 570 additions & 0 deletions

File tree

pkg/logs/patterns/processor/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/DataDog/datadog-agent/pkg/logs/patterns/processor
2+
3+
go 1.24.9
4+
5+
require github.com/stretchr/testify v1.11.1
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.1 // indirect
9+
github.com/pmezard/go-difflib v1.0.0 // indirect
10+
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
)

pkg/logs/patterns/processor/go.sum

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
// Package processor provides JSON-aware preprocessing for stateful log encoding.
7+
// It extracts message fields from JSON logs and serializes remaining fields into ordered json_context.
8+
package processor
9+
10+
import (
11+
"bytes"
12+
"encoding/json"
13+
"strings"
14+
)
15+
16+
// ExtractionResult contains the result of JSON preprocessing
17+
type ExtractionResult struct {
18+
// IsJSON indicates whether the input was valid JSON
19+
IsJSON bool
20+
// Message is the extracted message field (empty if not found or not JSON)
21+
Message string
22+
// JSONContext is the ordered, serialized remaining JSON fields (nil if not JSON or extraction failed)
23+
JSONContext []byte
24+
}
25+
26+
// Common top-level message field names (Layer 0)
27+
// These cover the vast majority of structured logs from popular logging libraries
28+
var topLevelMessageKeys = []string{
29+
"message",
30+
"msg",
31+
"log",
32+
"text",
33+
}
34+
35+
// Common nested paths (Layer 1 fallback)
36+
// Some applications wrap their log message in a data/event/payload envelope
37+
var nestedMessagePaths = []string{
38+
"data.message", // Generic data wrapper
39+
"event.message", // Event-based logs
40+
"payload.message", // Payload wrapper
41+
}
42+
43+
// PreprocessJSON attempts to extract a message field from JSON logs and serialize remaining fields.
44+
func PreprocessJSON(content []byte) ExtractionResult {
45+
fail := ExtractionResult{IsJSON: false}
46+
47+
// Check if it's a JSON object (handles leading whitespace)
48+
if !isJSONObject(content) {
49+
return fail
50+
}
51+
52+
// Parse JSON
53+
var data map[string]interface{}
54+
if err := json.Unmarshal(content, &data); err != nil {
55+
return fail
56+
}
57+
58+
// Try to extract message using layered strategy
59+
message, extractedPath := extractMessage(data)
60+
if message == "" {
61+
return fail
62+
}
63+
64+
// Remove the extracted message field from data for jsoncontext construction
65+
removeFieldByPath(data, extractedPath)
66+
67+
// If no fields remain after removing the message, keep json_context nil (avoid sending "{}").
68+
if len(data) == 0 {
69+
return ExtractionResult{
70+
IsJSON: true,
71+
Message: message,
72+
JSONContext: nil,
73+
}
74+
}
75+
76+
// Serialize remaining fields as JSON context.
77+
// encoding/json marshals maps with deterministic key ordering for better compression.
78+
jsonContext, err := json.Marshal(data)
79+
if err != nil {
80+
return fail
81+
}
82+
83+
return ExtractionResult{
84+
IsJSON: true,
85+
Message: message,
86+
JSONContext: jsonContext,
87+
}
88+
}
89+
90+
// extractMessage attempts to extract a message field using the layered strategy
91+
func extractMessage(data map[string]interface{}) (string, string) {
92+
// Layer 0: Top-level common keys
93+
for _, key := range topLevelMessageKeys {
94+
if val, ok := data[key]; ok {
95+
if str, ok := val.(string); ok && str != "" {
96+
return str, key
97+
}
98+
}
99+
}
100+
101+
// Layer 1: Common nested paths (e.g., data.message, event.message)
102+
for _, path := range nestedMessagePaths {
103+
if val := getValueByPath(data, path); val != "" {
104+
return val, path
105+
}
106+
}
107+
108+
return "", ""
109+
}
110+
111+
// getValueByPath retrieves a string value from nested JSON using dot notation
112+
// e.g., "data.message" -> data["data"]["message"]
113+
func getValueByPath(data map[string]interface{}, path string) string {
114+
parts := strings.Split(path, ".")
115+
if len(parts) == 0 {
116+
return ""
117+
}
118+
119+
current := data
120+
for _, part := range parts[:len(parts)-1] {
121+
val, ok := current[part]
122+
if !ok {
123+
return ""
124+
}
125+
126+
nextMap, ok := val.(map[string]interface{})
127+
if !ok {
128+
return ""
129+
}
130+
current = nextMap
131+
}
132+
133+
leaf, ok := current[parts[len(parts)-1]]
134+
if !ok {
135+
return ""
136+
}
137+
str, _ := leaf.(string)
138+
return str
139+
}
140+
141+
// removeFieldByPath removes a field from nested JSON using dot notation
142+
func removeFieldByPath(data map[string]interface{}, path string) {
143+
if path == "" {
144+
return
145+
}
146+
147+
parts := strings.Split(path, ".")
148+
if len(parts) == 1 {
149+
// Top-level key
150+
delete(data, parts[0])
151+
return
152+
}
153+
154+
// Navigate to parent
155+
current := data
156+
for i := 0; i < len(parts)-1; i++ {
157+
val, ok := current[parts[i]]
158+
if !ok {
159+
return
160+
}
161+
if nextMap, ok := val.(map[string]interface{}); ok {
162+
current = nextMap
163+
} else {
164+
return
165+
}
166+
}
167+
168+
// Delete the final key
169+
delete(current, parts[len(parts)-1])
170+
}
171+
172+
// isJSONObject checks if content is a JSON object, handling leading whitespace
173+
func isJSONObject(content []byte) bool {
174+
trimmed := bytes.TrimLeft(content, " \t\n\r")
175+
return len(trimmed) > 0 && trimmed[0] == '{'
176+
}

0 commit comments

Comments
 (0)