Skip to content

Commit 9c6f260

Browse files
vparfonovopenshift-merge-bot[bot]
authored andcommitted
LOG-6860: Support Splunk Metadata keys in ClusterLogForwarder
Signed-off-by: Vitalii Parfonov <[email protected]>
1 parent 06917ba commit 9c6f260

20 files changed

+1092
-60
lines changed

api/observability/v1/clusterlogforwarder_types.go

+9
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,15 @@ type ClusterLogForwarderList struct {
344344
Items []ClusterLogForwarder `json:"items"`
345345
}
346346

347+
// FieldPath represents a path to find a value for a given field. The format must be a value that can be converted to a
348+
// valid collector configuration. It is a dot delimited path to a field in the log record. It must start with a `.`.
349+
// The path can contain alphanumeric characters and underscores (a-zA-Z0-9_).
350+
// If segments contain characters outside of this range, the segment must be quoted.
351+
// Examples: `.kubernetes.namespace_name`, `.log_type`, '.kubernetes.labels.foobar', `.kubernetes.labels."foo-bar/baz"`
352+
//
353+
// +kubebuilder:validation:Pattern:=`^(\.[a-zA-Z0-9_]+|\."[^"]+")(\.[a-zA-Z0-9_]+|\."[^"]+")*$`
354+
type FieldPath string
355+
347356
func init() {
348357
SchemeBuilder.Register(&ClusterLogForwarder{}, &ClusterLogForwarderList{})
349358
}

api/observability/v1/filter_types.go

-9
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,6 @@ var (
4040
}
4141
)
4242

43-
// FieldPath represents a path to find a value for a given field. The format must a value that can be converted to a
44-
// valid collector configuration. It is a dot delimited path to a field in the log record. It must start with a `.`.
45-
// The path can contain alphanumeric characters and underscores (a-zA-Z0-9_).
46-
// If segments contain characters outside of this range, the segment must be quoted.
47-
// Examples: `.kubernetes.namespace_name`, `.log_type`, '.kubernetes.labels.foobar', `.kubernetes.labels."foo-bar/baz"`
48-
//
49-
// +kubebuilder:validation:Pattern:=`^(\.[a-zA-Z0-9_]+|\."[^"]+")(\.[a-zA-Z0-9_]+|\."[^"]+")*$`
50-
type FieldPath string
51-
5243
// FilterSpec defines a filter for log messages.
5344
//
5445
// +kubebuilder:validation:XValidation:rule="self.type != 'kubeAPIAudit' || has(self.kubeAPIAudit)", message="Additional type specific spec is required for the filter type"

api/observability/v1/output_types.go

+47
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,53 @@ type Splunk struct {
10451045
// +kubebuilder:validation:Pattern:=`^(([a-zA-Z0-9-_.\/])*(\{(\.[a-zA-Z0-9_]+|\."[^"]+")+((\|\|)(\.[a-zA-Z0-9_]+|\.?"[^"]+")+)*\|\|"[^"]*"\})*)*$`
10461046
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Index",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:text"}
10471047
Index string `json:"index,omitempty"`
1048+
1049+
// IndexedFields are the list of fields to be indexed by Splunk, increase storage usage, they should be used sparingly and only for high-value fields that provide significant search benefits.
1050+
// Nested fields are flattened into top-level fields.
1051+
// Field paths are joined using dot notation, and unsupported characters are replaced with underscores (_).
1052+
// Non-string values are automatically converted to strings (e.g., 3 → "3", true → "true").
1053+
// Object values serialized as JSON strings (e.g., { status: 200 } → "{\"status\":200}").
1054+
//
1055+
// Examples: [`.kubernetes`, `.log_type`, '.kubernetes.labels.foobar', `.kubernetes.labels."foo-bar/baz"`]
1056+
// +nullable
1057+
// +kubebuilder:validation:Optional
1058+
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Indexed Fields"
1059+
IndexedFields []FieldPath `json:"indexedFields,omitempty"`
1060+
1061+
// Source identifies the origin of a log event.
1062+
// The Source can be a combination of static and dynamic values consisting of field paths followed by `||` followed by another field path or a static value.
1063+
// A dynamic value is encased in single curly brackets `{}` and MUST end with a static fallback value separated with `||`.
1064+
// Static values can only contain alphanumeric characters along with dashes, underscores, dots and forward slashes.
1065+
// If not specified will be detected according to .log_source and .log_type value.
1066+
// Details see in: docs/features/logforwarding/outputs/splunk-forwarding.adoc
1067+
//
1068+
// Example:
1069+
//
1070+
// 1. foo-{.bar||"none"}
1071+
//
1072+
// 2. {.foo||.bar||"missing"}
1073+
//
1074+
// 3. foo.{.bar.baz||.qux.quux.corge||.grault||"nil"}-waldo.fred{.plugh||"none"}
1075+
//
1076+
// +kubebuilder:validation:Optional
1077+
// +kubebuilder:validation:Pattern:=`^(([a-zA-Z0-9-_.\/])*(\{(\.[a-zA-Z0-9_]+|\."[^"]+")+((\|\|)(\.[a-zA-Z0-9_]+|\.?"[^"]+")+)*\|\|"[^"]*"\})*)*$`
1078+
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Source",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:text"}
1079+
Source string `json:"source,omitempty"`
1080+
1081+
// PayloadKey specifies record field to use as payload.
1082+
// The PayloadKey must be a single field path.
1083+
//
1084+
// Field paths must only contain alphanumeric and underscores. Any field with other characters must be quoted.
1085+
//
1086+
// By default, payloadKey is not set, which means the complete log record is forwarded as the payload.
1087+
// Use payloadKey carefully. Selecting a single field as the payload may cause other important information in the
1088+
// log to be dropped, potentially leading to inconsistent or incomplete log events.
1089+
//
1090+
// Examples: `.kubernetes`, `.log_type`, '.kubernetes.labels.foobar', `.kubernetes.labels."foo-bar/baz"`
1091+
//
1092+
// +kubebuilder:validation:Optional
1093+
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Payload Key",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:text"}
1094+
PayloadKey FieldPath `json:"payloadKey,omitempty"`
10481095
}
10491096

10501097
// SyslogRFCType sets which RFC the generated messages conform to.

internal/generator/vector/helpers/helpers.go

+73
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
v1 "github.com/openshift/cluster-logging-operator/api/observability/v1"
66
"github.com/openshift/cluster-logging-operator/internal/constants"
77
"path/filepath"
8+
"regexp"
89
"sort"
10+
"strconv"
911
"strings"
1012
"sync"
1113

@@ -14,6 +16,9 @@ import (
1416

1517
const VectorSecretID = "kubernetes_secret"
1618

19+
// Match quoted strings like "foo" or "foo/bar-baz"
20+
var quoteRegex = regexp.MustCompile(`^".+"$`)
21+
1722
var (
1823
Replacer = strings.NewReplacer(" ", "_", "-", "_", ".", "_")
1924
listenAllAddress string
@@ -80,3 +85,71 @@ func SecretFrom(secretKey *v1.SecretReference) string {
8085
}
8186
return ""
8287
}
88+
89+
// GenerateQuotedPathSegmentArrayStr generates the final string of the array of array of path segments
90+
// and array of flattened path with replaced not allowed symbols to feed into VRL
91+
// E.g
92+
// [.kubernetes.namespace_labels."bar/baz0-9.test"] -> ([["kubernetes","namespace_labels","bar/baz0-9.test"]], ["_kubernetes_namespace_labels_bar_baz0-9_test"])
93+
func GenerateQuotedPathSegmentArrayStr(fieldPathArray []v1.FieldPath) (string, string) {
94+
var quotedPathArray []string
95+
var flattenedArray []string
96+
97+
for _, fieldPath := range fieldPathArray {
98+
pathStr := string(fieldPath)
99+
100+
if strings.ContainsAny(pathStr, "/.") {
101+
flat := strings.NewReplacer(".", "_", "\"", "", "/", "_").Replace(pathStr)
102+
flat = strings.TrimPrefix(flat, "_")
103+
flattenedArray = append(flattenedArray, strconv.Quote(flat))
104+
}
105+
106+
splitSegments := SplitPath(pathStr)
107+
quotedSegments := QuotePathSegments(splitSegments)
108+
quotedPathArray = append(quotedPathArray, fmt.Sprintf("[%s]", strings.Join(quotedSegments, ",")))
109+
}
110+
111+
return fmt.Sprintf("[%s]", strings.Join(quotedPathArray, ",")),
112+
fmt.Sprintf("[%s]", strings.Join(flattenedArray, ","))
113+
}
114+
115+
// SplitPath splits a fieldPath by `.` and reassembles the quoted path segments that also contain `.`
116+
// Example: `.foo."@some"."d.f.g.o111-22/333".foo_bar`
117+
// Resultant Array: ["foo","@some",`"d.f.g.o111-22/333"`,"foo_bar"]
118+
func SplitPath(path string) []string {
119+
var result []string
120+
121+
segments := strings.Split(path, ".")
122+
123+
var currSegment string
124+
for _, part := range segments {
125+
if part == "" {
126+
continue
127+
} else if strings.HasPrefix(part, `"`) && strings.HasSuffix(part, `"`) {
128+
result = append(result, part)
129+
} else if strings.HasPrefix(part, `"`) {
130+
currSegment = part
131+
} else if strings.HasSuffix(part, `"`) {
132+
currSegment += "." + part
133+
result = append(result, currSegment)
134+
currSegment = ""
135+
} else if currSegment != "" {
136+
currSegment += "." + part
137+
} else {
138+
result = append(result, part)
139+
}
140+
}
141+
return result
142+
}
143+
144+
// QuotePathSegments quotes all path segments as needed for VRL
145+
func QuotePathSegments(pathArray []string) []string {
146+
for i, field := range pathArray {
147+
// Don't surround in quotes if already quoted
148+
if quoteRegex.MatchString(field) {
149+
continue
150+
}
151+
// Put quotes around path segments
152+
pathArray[i] = fmt.Sprintf("%q", field)
153+
}
154+
return pathArray
155+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package helpers
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
obs "github.com/openshift/cluster-logging-operator/api/observability/v1"
7+
)
8+
9+
var _ = Describe("helpers functions", func() {
10+
Context("#GenerateQuotedPathSegmentArrayStr", func() {
11+
It("should generate array of path segments and flat path for single value", func() {
12+
pathExpression := []obs.FieldPath{`.kubernetes.labels.foo`}
13+
expectedArrayPath := `[["kubernetes","labels","foo"]]`
14+
expectedFlatPath := `["kubernetes_labels_foo"]`
15+
arrayPath, flatPath := GenerateQuotedPathSegmentArrayStr(pathExpression)
16+
Expect(arrayPath).To(Equal(expectedArrayPath))
17+
Expect(flatPath).To(Equal(expectedFlatPath))
18+
})
19+
It("should generate array of path segments and flat path for multiple value", func() {
20+
pathExpression := []obs.FieldPath{`.kubernetes.labels.foo`, `.kubernetes.labels.bar`}
21+
expectedArrayPath := `[["kubernetes","labels","foo"],["kubernetes","labels","bar"]]`
22+
expectedFlatPath := `["kubernetes_labels_foo","kubernetes_labels_bar"]`
23+
arrayPath, flatPath := GenerateQuotedPathSegmentArrayStr(pathExpression)
24+
Expect(arrayPath).To(Equal(expectedArrayPath))
25+
Expect(flatPath).To(Equal(expectedFlatPath))
26+
})
27+
It("should generate array of path segments and escaped flat path", func() {
28+
pathExpression := []obs.FieldPath{`.kubernetes.labels."bar/baz0-9.test"`}
29+
expectedArrayPath := `[["kubernetes","labels","bar/baz0-9.test"]]`
30+
expectedFlatPath := `["kubernetes_labels_bar_baz0-9_test"]`
31+
arrayPath, flatPath := GenerateQuotedPathSegmentArrayStr(pathExpression)
32+
Expect(arrayPath).To(Equal(expectedArrayPath))
33+
Expect(flatPath).To(Equal(expectedFlatPath))
34+
})
35+
It("should generate array of path segments and escaped flat path", func() {
36+
pathExpression := []obs.FieldPath{`.foo.bar."foo.bar.baz-ok".foo123."bar/baz0-9.test"`, `.foo.bar`}
37+
expectedArrayPath := `[["foo","bar","foo.bar.baz-ok","foo123","bar/baz0-9.test"],["foo","bar"]]`
38+
expectedFlatPath := `["foo_bar_foo_bar_baz-ok_foo123_bar_baz0-9_test","foo_bar"]`
39+
arrayPath, flatPath := GenerateQuotedPathSegmentArrayStr(pathExpression)
40+
Expect(arrayPath).To(Equal(expectedArrayPath))
41+
Expect(flatPath).To(Equal(expectedFlatPath))
42+
})
43+
})
44+
45+
DescribeTable("#SplitPath generates correct array of path segments", func(path string, expectedArray []string) {
46+
Expect(SplitPath(path)).To(Equal(expectedArray))
47+
},
48+
Entry("with single segment", `.foo`, []string{"foo"}),
49+
Entry("with 2 segments", `.foo.bar`, []string{"foo", "bar"}),
50+
Entry("with first segment in quotes", `."@foobar"`, []string{`"@foobar"`}),
51+
Entry("with 1 quoted segment and one with quotes", `.foo."bar111-22/333"`, []string{"foo", `"bar111-22/333"`}),
52+
Entry("with 2 non quoted segments and one quoted segment ", `.foo.bar."baz111-22/333"`, []string{"foo", "bar", `"baz111-22/333"`}),
53+
Entry("with multiple quoted and unquoted segments", `.foo."@some"."d.f.g.o111-22/333".foo_bar`, []string{"foo", `"@some"`, `"d.f.g.o111-22/333"`, "foo_bar"}))
54+
55+
DescribeTable("#QuotePathSegments generates array with path segments quoted", func(pathSegments []string, expectedArray []string) {
56+
Expect(QuotePathSegments(pathSegments)).To(Equal(expectedArray))
57+
},
58+
Entry("single value", []string{"foo"}, []string{`"foo"`}),
59+
Entry("multiple value", []string{"foo", "bar.zip", `"foo-bar"`}, []string{`"foo"`, `"bar.zip"`, `"foo-bar"`}),
60+
)
61+
})

0 commit comments

Comments
 (0)