Skip to content

Commit 2587f34

Browse files
authored
feat(lambda-promtail): Improve relabel configuration parsing and testing (#16100)
1 parent 0baa6a7 commit 2587f34

File tree

3 files changed

+250
-8
lines changed

3 files changed

+250
-8
lines changed

‎tools/lambda-promtail/lambda-promtail/main.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,17 @@ func setupArguments() {
103103
batchSize, _ = strconv.Atoi(batch)
104104
}
105105

106-
print := os.Getenv("PRINT_LOG_LINE")
107106
printLogLine = true
108-
if strings.EqualFold(print, "false") {
107+
if strings.EqualFold(os.Getenv("PRINT_LOG_LINE"), "false") {
109108
printLogLine = false
110109
}
111110
s3Clients = make(map[string]*s3.Client)
112111

113-
// Parse relabel configs from environment variable
114-
if relabelConfigsRaw := os.Getenv("RELABEL_CONFIGS"); relabelConfigsRaw != "" {
115-
if err := json.Unmarshal([]byte(relabelConfigsRaw), &relabelConfigs); err != nil {
116-
panic(fmt.Errorf("failed to parse RELABEL_CONFIGS: %v", err))
117-
}
112+
promConfigs, err := parseRelabelConfigs(os.Getenv("RELABEL_CONFIGS"))
113+
if err != nil {
114+
panic(err)
118115
}
116+
relabelConfigs = promConfigs
119117
}
120118

121119
func parseExtraLabels(extraLabelsRaw string, omitPrefix bool) (model.LabelSet, error) {
@@ -131,7 +129,7 @@ func parseExtraLabels(extraLabelsRaw string, omitPrefix bool) (model.LabelSet, e
131129
}
132130

133131
if len(extraLabelsSplit)%2 != 0 {
134-
return nil, fmt.Errorf(invalidExtraLabelsError)
132+
return nil, errors.New(invalidExtraLabelsError)
135133
}
136134
for i := 0; i < len(extraLabelsSplit); i += 2 {
137135
extractedLabels[model.LabelName(prefix+extraLabelsSplit[i])] = model.LabelValue(extraLabelsSplit[i+1])
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/prometheus/common/model"
8+
"github.com/prometheus/prometheus/model/relabel"
9+
)
10+
11+
// copy and modification of github.com/prometheus/prometheus/model/relabel/relabel.go
12+
// reason: the custom types in github.com/prometheus/prometheus/model/relabel/relabel.go are difficult to unmarshal
13+
type RelabelConfig struct {
14+
// A list of labels from which values are taken and concatenated
15+
// with the configured separator in order.
16+
SourceLabels []string `json:"source_labels,omitempty"`
17+
// Separator is the string between concatenated values from the source labels.
18+
Separator string `json:"separator,omitempty"`
19+
// Regex against which the concatenation is matched.
20+
Regex string `json:"regex,omitempty"`
21+
// Modulus to take of the hash of concatenated values from the source labels.
22+
Modulus uint64 `json:"modulus,omitempty"`
23+
// TargetLabel is the label to which the resulting string is written in a replacement.
24+
// Regexp interpolation is allowed for the replace action.
25+
TargetLabel string `json:"target_label,omitempty"`
26+
// Replacement is the regex replacement pattern to be used.
27+
Replacement string `json:"replacement,omitempty"`
28+
// Action is the action to be performed for the relabeling.
29+
Action string `json:"action,omitempty"`
30+
}
31+
32+
// UnmarshalJSON implements the json.Unmarshaler interface.
33+
func (rc *RelabelConfig) UnmarshalJSON(data []byte) error {
34+
*rc = RelabelConfig{
35+
Action: string(relabel.Replace),
36+
Separator: ";",
37+
Regex: "(.*)",
38+
Replacement: "$1",
39+
}
40+
type plain RelabelConfig
41+
if err := json.Unmarshal(data, (*plain)(rc)); err != nil {
42+
return err
43+
}
44+
return nil
45+
}
46+
47+
// ToPrometheusConfig converts our JSON-friendly RelabelConfig to the Prometheus RelabelConfig
48+
func (rc *RelabelConfig) ToPrometheusConfig() (*relabel.Config, error) {
49+
var regex relabel.Regexp
50+
if rc.Regex != "" {
51+
var err error
52+
regex, err = relabel.NewRegexp(rc.Regex)
53+
if err != nil {
54+
return nil, fmt.Errorf("invalid regex %q: %w", rc.Regex, err)
55+
}
56+
} else {
57+
regex = relabel.DefaultRelabelConfig.Regex
58+
}
59+
60+
action := relabel.Action(rc.Action)
61+
if rc.Action == "" {
62+
action = relabel.DefaultRelabelConfig.Action
63+
}
64+
65+
separator := rc.Separator
66+
if separator == "" {
67+
separator = relabel.DefaultRelabelConfig.Separator
68+
}
69+
70+
replacement := rc.Replacement
71+
if replacement == "" {
72+
replacement = relabel.DefaultRelabelConfig.Replacement
73+
}
74+
75+
sourceLabels := make(model.LabelNames, 0, len(rc.SourceLabels))
76+
for _, l := range rc.SourceLabels {
77+
sourceLabels = append(sourceLabels, model.LabelName(l))
78+
}
79+
80+
cfg := &relabel.Config{
81+
SourceLabels: sourceLabels,
82+
Separator: separator,
83+
Regex: regex,
84+
Modulus: rc.Modulus,
85+
TargetLabel: rc.TargetLabel,
86+
Replacement: replacement,
87+
Action: action,
88+
}
89+
90+
if err := cfg.Validate(); err != nil {
91+
return nil, fmt.Errorf("invalid relabel config: %w", err)
92+
}
93+
return cfg, nil
94+
}
95+
96+
func ToPrometheusConfigs(cfgs []*RelabelConfig) ([]*relabel.Config, error) {
97+
promConfigs := make([]*relabel.Config, 0, len(cfgs))
98+
for _, cfg := range cfgs {
99+
promCfg, err := cfg.ToPrometheusConfig()
100+
if err != nil {
101+
return nil, fmt.Errorf("invalid relabel config: %w", err)
102+
}
103+
promConfigs = append(promConfigs, promCfg)
104+
}
105+
return promConfigs, nil
106+
}
107+
108+
func parseRelabelConfigs(relabelConfigsRaw string) ([]*relabel.Config, error) {
109+
if relabelConfigsRaw == "" {
110+
return nil, nil
111+
}
112+
113+
var relabelConfigs []*RelabelConfig
114+
115+
if err := json.Unmarshal([]byte(relabelConfigsRaw), &relabelConfigs); err != nil {
116+
return nil, fmt.Errorf("failed to parse RELABEL_CONFIGS: %v", err)
117+
}
118+
promConfigs, err := ToPrometheusConfigs(relabelConfigs)
119+
if err != nil {
120+
return nil, fmt.Errorf("failed to parse RELABEL_CONFIGS: %v", err)
121+
}
122+
return promConfigs, nil
123+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/prometheus/prometheus/model/relabel"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/grafana/regexp"
10+
)
11+
12+
func TestParseRelabelConfigs(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
input string
16+
want []*relabel.Config
17+
wantErr bool
18+
}{
19+
{
20+
name: "empty input",
21+
input: "",
22+
want: nil,
23+
wantErr: false,
24+
},
25+
{
26+
name: "default config",
27+
input: `[{"target_label": "new_label"}]`,
28+
want: []*relabel.Config{
29+
{
30+
TargetLabel: "new_label",
31+
Action: relabel.Replace,
32+
Regex: relabel.Regexp{Regexp: regexp.MustCompile("(.*)")},
33+
Replacement: "$1",
34+
},
35+
},
36+
wantErr: false,
37+
},
38+
{
39+
name: "invalid JSON",
40+
input: "invalid json",
41+
wantErr: true,
42+
},
43+
{
44+
name: "valid single config",
45+
input: `[{
46+
"source_labels": ["__name__"],
47+
"regex": "my_metric_.*",
48+
"target_label": "new_label",
49+
"replacement": "foo",
50+
"action": "replace"
51+
}]`,
52+
wantErr: false,
53+
},
54+
{
55+
name: "invalid regex",
56+
input: `[{
57+
"source_labels": ["__name__"],
58+
"regex": "[[invalid regex",
59+
"target_label": "new_label",
60+
"action": "replace"
61+
}]`,
62+
wantErr: true,
63+
},
64+
{
65+
name: "multiple valid configs",
66+
input: `[
67+
{
68+
"source_labels": ["__name__"],
69+
"regex": "my_metric_.*",
70+
"target_label": "new_label",
71+
"replacement": "foo",
72+
"action": "replace"
73+
},
74+
{
75+
"source_labels": ["label1", "label2"],
76+
"separator": ";",
77+
"regex": "val1;val2",
78+
"target_label": "combined",
79+
"action": "replace"
80+
}
81+
]`,
82+
wantErr: false,
83+
},
84+
{
85+
name: "invalid action",
86+
input: `[{
87+
"source_labels": ["__name__"],
88+
"regex": "my_metric_.*",
89+
"target_label": "new_label",
90+
"action": "invalid_action"
91+
}]`,
92+
wantErr: false,
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
got, err := parseRelabelConfigs(tt.input)
99+
if tt.wantErr {
100+
require.Error(t, err)
101+
return
102+
}
103+
require.NoError(t, err)
104+
105+
if tt.input == "" {
106+
require.Nil(t, got)
107+
return
108+
}
109+
110+
require.NotNil(t, got)
111+
// For valid configs, verify they can be used for relabeling
112+
// This implicitly tests that the conversion was successful
113+
if len(got) > 0 {
114+
for _, cfg := range got {
115+
require.NotNil(t, cfg)
116+
require.NotEmpty(t, cfg.Action)
117+
}
118+
}
119+
})
120+
}
121+
}

0 commit comments

Comments
 (0)