Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add Custom columns labels support (#605) #2953

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions internal/model1/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@

package model1

import "reflect"
import (
"reflect"
)

// Fields represents a collection of row fields.
type Fields []string

// Customize returns a subset of fields.
func (f Fields) Customize(cols []int, out Fields) {
func (f Fields) Customize(cols []int, out Fields, extractionInfoBag ExtractionInfoBag) {
for i, c := range cols {

if c < 0 {
out[i] = NAValue
out[i] = getValueOfInvalidColumn(f, i, extractionInfoBag)
continue
}

if c < len(f) {
out[i] = f[c]
}
Expand All @@ -39,3 +43,19 @@ func (f Fields) Clone() Fields {

return cp
}

func getValueOfInvalidColumn(f Fields, i int, extractionInfoBag ExtractionInfoBag) string {

extractionInfo, ok := extractionInfoBag[i]

// If the extractionInfo is existed in extractionInfoBag,
// meaning this column has to retrieve the actual value from other field.
// For example: `LABELS[kubernetes.io/hostname]` needs to extract the value from column `LABELS`
if ok {
idxInFields := extractionInfo.IdxInFields
escapedKey := escapeDots(extractionInfo.Key)
return extractValueFromField(escapedKey, f[idxInFields])
}

return NAValue
}
57 changes: 51 additions & 6 deletions internal/model1/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,23 @@ package model1

import (
"reflect"
"regexp"

"github.com/rs/zerolog/log"
)

const ageCol = "AGE"

// ExtractionInfo stores data for a field to extract value from another field
type ExtractionInfo struct {
IdxInFields int
HeaderName string
Key string
}

// ExtractionInfoBag store ExtractionInfo by using the index of the column
type ExtractionInfoBag map[int]ExtractionInfo

// HeaderColumn represent a table header.
type HeaderColumn struct {
Name string
Expand Down Expand Up @@ -64,18 +75,52 @@ func (h Header) Labelize(cols []int, labelCol int, rr *RowEvents) Header {
}

// MapIndices returns a collection of mapped column indices based of the requested columns.
func (h Header) MapIndices(cols []string, wide bool) []int {
// And the extraction information used to extract a value for a field from another field.
func (h Header) MapIndices(cols []string, wide bool) ([]int, ExtractionInfoBag) {
ii := make([]int, 0, len(cols))
cc := make(map[int]struct{}, len(cols))

eib := make(ExtractionInfoBag)
regex, _ := regexp.Compile(`^(?<HEADER_NAME>.*)\[(?<KEY>.*)\]$`)

for _, col := range cols {
idx, ok := h.IndexOf(col, true)
if !ok {
log.Warn().Msgf("Column %q not found on resource", col)
}

ii, cc[idx] = append(ii, idx), struct{}{}

// Continue to next iteration if the column is found
if ok {
continue
}

log.Warn().Msgf("Column %q not found on resource", col)

// Continue to next iteration ff the column doesn't match the regex
if !regex.MatchString(col) {
log.Warn().Msgf("Column %q doesn't match regex pattern", col)
continue
}

// Check if the column matches the pattern
// For example: `LABELS[beta.kubernetes.io/os]` will match
matches := regex.FindStringSubmatch(col)
headerName := matches[1]
key := matches[2]

// now only support LABELS
if headerName != "LABELS" {
log.Warn().Msgf("Custom Column %q is not supported", col)
continue
}

log.Warn().Msgf("Custom column %q is extracting value with header name: %q and key: %q", col, headerName, key)
currentIdx := len(ii) - 1
idxInFields, _ := h.IndexOf(headerName, true)
eib[currentIdx] = ExtractionInfo{idxInFields, headerName, key}
}

if !wide {
return ii
return ii, eib
}

for i := range h {
Expand All @@ -84,7 +129,7 @@ func (h Header) MapIndices(cols []string, wide bool) []int {
}
ii = append(ii, i)
}
return ii
return ii, eib
}

// Customize builds a header from custom col definitions.
Expand Down
22 changes: 19 additions & 3 deletions internal/model1/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,49 @@ func TestHeaderMapIndices(t *testing.T) {
cols []string
wide bool
e []int
eib model1.ExtractionInfoBag
}{
"all": {
h1: makeHeader(),
cols: []string{"A", "B", "C"},
e: []int{0, 1, 2},
eib: model1.ExtractionInfoBag{},
},
"reverse": {
h1: makeHeader(),
cols: []string{"C", "B", "A"},
e: []int{2, 1, 0},
eib: model1.ExtractionInfoBag{},
},
"missing": {
h1: makeHeader(),
cols: []string{"Duh", "B", "A"},
e: []int{-1, 1, 0},
eib: model1.ExtractionInfoBag{},
},
"skip": {
h1: makeHeader(),
cols: []string{"C", "A"},
e: []int{2, 0},
eib: model1.ExtractionInfoBag{},
},
"labels": {
h1: makeHeader(),
cols: []string{"A", "LABELS[kubernetes.io/hostname]", "B", "LABELS[topology.kubernetes.io/region]"},
e: []int{0, -1, 1, -1},
eib: model1.ExtractionInfoBag{
1: model1.ExtractionInfo{3, "LABELS", "kubernetes.io/hostname"},
3: model1.ExtractionInfo{3, "LABELS", "topology.kubernetes.io/region"},
},
},
}

for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
ii := u.h1.MapIndices(u.cols, u.wide)
ii, eib := u.h1.MapIndices(u.cols, u.wide)
assert.Equal(t, u.e, ii)
assert.Equal(t, u.eib, eib)
})
}
}
Expand Down Expand Up @@ -264,11 +279,11 @@ func TestHeaderColumns(t *testing.T) {
},
"regular": {
h: makeHeader(),
e: []string{"A", "C"},
e: []string{"A", "C", "LABELS"},
},
"wide": {
h: makeHeader(),
e: []string{"A", "B", "C"},
e: []string{"A", "B", "C", "LABELS"},
wide: true,
},
}
Expand Down Expand Up @@ -314,5 +329,6 @@ func makeHeader() model1.Header {
model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true},
model1.HeaderColumn{Name: "C"},
model1.HeaderColumn{Name: "LABELS"},
}
}
21 changes: 21 additions & 0 deletions internal/model1/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package model1
import (
"fmt"
"math"
"regexp"
"sort"
"strings"

Expand Down Expand Up @@ -164,3 +165,23 @@ func lessNumber(s1, s2 string) bool {

return sortorder.NaturalLess(v1, v2)
}

// Escape dots from the string
// For example: `kubernetes.io/hostname` needs to be escaped to `kubernetes\.io/hostname`
func escapeDots(s string) string {
return strings.ReplaceAll(s, ".", "\\.")
}

func extractValueFromField(key string, field string) string {
// Extract the value by using regex
pattern := fmt.Sprintf(`%s=([^ ]+)`, key)
regex := regexp.MustCompile(pattern)

// Find the value in the field that store original values
matches := regex.FindStringSubmatch(field)
if len(matches) > 1 {
return matches[1]
}

return ""
}
26 changes: 26 additions & 0 deletions internal/model1/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,29 @@ func BenchmarkDurationToSecond(b *testing.B) {
durationToSeconds(t)
}
}

func TestEscapeDots(t *testing.T) {
var s string

s = escapeDots("kubernetes.io/hostname")
assert.Equal(t, "kubernetes\\.io/hostname", s)

s = escapeDots("kubernetes-io/hostname")
assert.Equal(t, "kubernetes-io/hostname", s)
}

func TestExtractValueFromFields(t *testing.T) {
k := escapeDots("kubernetes.io/hostname")
f := "kubernetes.io/arch=amd64 kubernetes.io/hostname=a-b-c-d kubernetes.io/os=linux"

var s string

s = extractValueFromField(k, f)
assert.Equal(t, "a-b-c-d", s)

s = extractValueFromField(k, "kubernetes.io/hostname=e-f-g-h "+f)
assert.Equal(t, "e-f-g-h", s)

s = extractValueFromField("random-key", f)
assert.Equal(t, "", s)
}
4 changes: 2 additions & 2 deletions internal/model1/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ func (r Row) Labelize(cols []int, labelCol int, labels []string) Row {
}

// Customize returns a row subset based on given col indices.
func (r Row) Customize(cols []int) Row {
func (r Row) Customize(cols []int, extractionInfoBag ExtractionInfoBag) Row {
out := NewRow(len(cols))
r.Fields.Customize(cols, out.Fields)
r.Fields.Customize(cols, out.Fields, extractionInfoBag)
out.ID = r.ID

return out
Expand Down
8 changes: 4 additions & 4 deletions internal/model1/row_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (r RowEvent) Clone() RowEvent {
}

// Customize returns a new subset based on the given column indices.
func (r RowEvent) Customize(cols []int) RowEvent {
func (r RowEvent) Customize(cols []int, extractionInfoBag ExtractionInfoBag) RowEvent {
delta := r.Deltas
if !r.Deltas.IsBlank() {
delta = make(DeltaRow, len(cols))
Expand All @@ -57,7 +57,7 @@ func (r RowEvent) Customize(cols []int) RowEvent {
return RowEvent{
Kind: r.Kind,
Deltas: delta,
Row: r.Row.Customize(cols),
Row: r.Row.Customize(cols, extractionInfoBag),
}
}

Expand Down Expand Up @@ -158,10 +158,10 @@ func (r *RowEvents) Labelize(cols []int, labelCol int, labels []string) *RowEven
}

// Customize returns custom row events based on columns layout.
func (r *RowEvents) Customize(cols []int) *RowEvents {
func (r *RowEvents) Customize(cols []int, extractionInfoBag ExtractionInfoBag) *RowEvents {
ee := make([]RowEvent, 0, len(cols))
for _, re := range r.events {
ee = append(ee, re.Customize(cols))
ee = append(ee, re.Customize(cols, extractionInfoBag))
}

return NewRowEventsWithEvts(ee...)
Expand Down
6 changes: 4 additions & 2 deletions internal/model1/row_event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func TestRowEventCustomize(t *testing.T) {
uu := map[string]struct {
re1, e model1.RowEvent
cols []int
eib model1.ExtractionInfoBag
}{
"empty": {
re1: model1.RowEvent{
Expand Down Expand Up @@ -101,7 +102,7 @@ func TestRowEventCustomize(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.re1.Customize(u.cols))
assert.Equal(t, u.e, u.re1.Customize(u.cols, u.eib))
})
}
}
Expand Down Expand Up @@ -297,6 +298,7 @@ func TestRowEventsCustomize(t *testing.T) {
uu := map[string]struct {
re, e *model1.RowEvents
cols []int
eib model1.ExtractionInfoBag
}{
"same": {
re: model1.NewRowEventsWithEvts(
Expand Down Expand Up @@ -355,7 +357,7 @@ func TestRowEventsCustomize(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.re.Customize(u.cols))
assert.Equal(t, u.e, u.re.Customize(u.cols, u.eib))
})
}
}
Expand Down
Loading