From a20a6f4f796244441f1cb9167dab0569b06f937f Mon Sep 17 00:00:00 2001 From: Dominik Rosiek <58699848+sumo-drosiek@users.noreply.github.com> Date: Mon, 7 Dec 2020 22:13:14 +0100 Subject: [PATCH] Change fields type and add sourceFormat(s) (#1756) --- exporter/sumologicexporter/exporter.go | 11 +- exporter/sumologicexporter/fields.go | 35 ++++++ exporter/sumologicexporter/fields_test.go | 32 ++++++ exporter/sumologicexporter/filter.go | 31 +---- exporter/sumologicexporter/filter_test.go | 6 +- exporter/sumologicexporter/sender.go | 43 ++++--- exporter/sumologicexporter/sender_test.go | 61 +++++----- exporter/sumologicexporter/source_format.go | 86 ++++++++++++++ .../sumologicexporter/source_format_test.go | 108 ++++++++++++++++++ 9 files changed, 338 insertions(+), 75 deletions(-) create mode 100644 exporter/sumologicexporter/fields.go create mode 100644 exporter/sumologicexporter/fields_test.go create mode 100644 exporter/sumologicexporter/source_format.go create mode 100644 exporter/sumologicexporter/source_format_test.go diff --git a/exporter/sumologicexporter/exporter.go b/exporter/sumologicexporter/exporter.go index 58427bbdef4c..8e476832880f 100644 --- a/exporter/sumologicexporter/exporter.go +++ b/exporter/sumologicexporter/exporter.go @@ -25,7 +25,8 @@ import ( ) type sumologicexporter struct { - config *Config + config *Config + sources sourceFormats } func initExporter(cfg *Config) (*sumologicexporter, error) { @@ -56,8 +57,14 @@ func initExporter(cfg *Config) (*sumologicexporter, error) { return nil, errors.New("endpoint is not set") } + sfs, err := newSourceFormats(cfg) + if err != nil { + return nil, err + } + se := &sumologicexporter{ - config: cfg, + config: cfg, + sources: sfs, } return se, nil diff --git a/exporter/sumologicexporter/fields.go b/exporter/sumologicexporter/fields.go new file mode 100644 index 000000000000..354033de52a8 --- /dev/null +++ b/exporter/sumologicexporter/fields.go @@ -0,0 +1,35 @@ +// Copyright 2020 OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sumologicexporter + +import ( + "fmt" + "sort" + "strings" +) + +// fields represents metadata +type fields map[string]string + +// string returns fields as ordered key=value string with `, ` as separator +func (f fields) string() string { + rv := make([]string, 0, len(f)) + for k, v := range f { + rv = append(rv, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(rv) + + return strings.Join(rv, ", ") +} diff --git a/exporter/sumologicexporter/fields_test.go b/exporter/sumologicexporter/fields_test.go new file mode 100644 index 000000000000..44fd2be9ac2a --- /dev/null +++ b/exporter/sumologicexporter/fields_test.go @@ -0,0 +1,32 @@ +// Copyright 2020 OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sumologicexporter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFieldsAsString(t *testing.T) { + expected := "key1=value1, key2=value2, key3=value3" + flds := fields{ + "key1": "value1", + "key3": "value3", + "key2": "value2", + } + + assert.Equal(t, expected, flds.string()) +} diff --git a/exporter/sumologicexporter/filter.go b/exporter/sumologicexporter/filter.go index 5dac39a462d6..5bb7fe9519b3 100644 --- a/exporter/sumologicexporter/filter.go +++ b/exporter/sumologicexporter/filter.go @@ -15,10 +15,7 @@ package sumologicexporter import ( - "fmt" "regexp" - "sort" - "strings" "go.opentelemetry.io/collector/consumer/pdata" tracetranslator "go.opentelemetry.io/collector/translator/trace" @@ -28,9 +25,6 @@ type filter struct { regexes []*regexp.Regexp } -// fields represents concatenated metadata -type fields string - func newFilter(flds []string) (filter, error) { metadataRegexes := make([]*regexp.Regexp, len(flds)) @@ -48,9 +42,9 @@ func newFilter(flds []string) (filter, error) { }, nil } -// filterIn returns map of strings which matches at least one of the filter regexes -func (f *filter) filterIn(attributes pdata.AttributeMap) map[string]string { - returnValue := make(map[string]string) +// filterIn returns fields which match at least one of the filter regexes +func (f *filter) filterIn(attributes pdata.AttributeMap) fields { + returnValue := make(fields) attributes.ForEach(func(k string, v pdata.AttributeValue) { for _, regex := range f.regexes { @@ -63,9 +57,9 @@ func (f *filter) filterIn(attributes pdata.AttributeMap) map[string]string { return returnValue } -// filterOut returns map of strings which doesn't match any of the filter regexes -func (f *filter) filterOut(attributes pdata.AttributeMap) map[string]string { - returnValue := make(map[string]string) +// filterOut returns fields which don't match any of the filter regexes +func (f *filter) filterOut(attributes pdata.AttributeMap) fields { + returnValue := make(fields) attributes.ForEach(func(k string, v pdata.AttributeValue) { for _, regex := range f.regexes { @@ -77,16 +71,3 @@ func (f *filter) filterOut(attributes pdata.AttributeMap) map[string]string { }) return returnValue } - -// getMetadata builds string which represents metadata in alphabetical order -func (f *filter) getMetadata(attributes pdata.AttributeMap) fields { - attrs := f.filterIn(attributes) - metadata := make([]string, 0, len(attrs)) - - for k, v := range attrs { - metadata = append(metadata, fmt.Sprintf("%s=%s", k, v)) - } - sort.Strings(metadata) - - return fields(strings.Join(metadata, ", ")) -} diff --git a/exporter/sumologicexporter/filter_test.go b/exporter/sumologicexporter/filter_test.go index 80acc6069b4f..c0dc4b1fa638 100644 --- a/exporter/sumologicexporter/filter_test.go +++ b/exporter/sumologicexporter/filter_test.go @@ -34,8 +34,8 @@ func TestGetMetadata(t *testing.T) { f, err := newFilter(regexes) require.NoError(t, err) - metadata := f.getMetadata(attributes) - const expected fields = "key1=value1, key2=value2, key3=value3" + metadata := f.filterIn(attributes) + expected := fields{"key1": "value1", "key2": "value2", "key3": "value3"} assert.Equal(t, expected, metadata) } @@ -52,7 +52,7 @@ func TestFilterOutMetadata(t *testing.T) { require.NoError(t, err) data := f.filterOut(attributes) - expected := map[string]string{ + expected := fields{ "additional_key2": "value2", "additional_key3": "value3", } diff --git a/exporter/sumologicexporter/sender.go b/exporter/sumologicexporter/sender.go index b8577be84778..a5b1600baaa8 100644 --- a/exporter/sumologicexporter/sender.go +++ b/exporter/sumologicexporter/sender.go @@ -36,11 +36,12 @@ type appendResponse struct { } type sender struct { - buffer []pdata.LogRecord - config *Config - client *http.Client - filter filter - ctx context.Context + buffer []pdata.LogRecord + config *Config + client *http.Client + filter filter + ctx context.Context + sources sourceFormats } const ( @@ -55,17 +56,25 @@ func newAppendResponse() appendResponse { } } -func newSender(ctx context.Context, cfg *Config, cl *http.Client, f filter) *sender { +func newSender( + ctx context.Context, + cfg *Config, + cl *http.Client, + f filter, + s sourceFormats, +) *sender { return &sender{ - config: cfg, - client: cl, - filter: f, - ctx: ctx, + config: cfg, + client: cl, + filter: f, + ctx: ctx, + sources: s, } } // send sends data to sumologic func (s *sender) send(pipeline PipelineType, body io.Reader, flds fields) error { + // Add headers req, err := http.NewRequestWithContext(s.ctx, http.MethodPost, s.config.HTTPClientSettings.Endpoint, body) if err != nil { @@ -74,22 +83,22 @@ func (s *sender) send(pipeline PipelineType, body io.Reader, flds fields) error req.Header.Add("X-Sumo-Client", s.config.Client) - if len(s.config.SourceHost) > 0 { - req.Header.Add("X-Sumo-Host", s.config.SourceHost) + if s.sources.host.isSet() { + req.Header.Add("X-Sumo-Host", s.sources.host.format(flds)) } - if len(s.config.SourceName) > 0 { - req.Header.Add("X-Sumo-Name", s.config.SourceName) + if s.sources.name.isSet() { + req.Header.Add("X-Sumo-Name", s.sources.name.format(flds)) } - if len(s.config.SourceCategory) > 0 { - req.Header.Add("X-Sumo-Category", s.config.SourceCategory) + if s.sources.category.isSet() { + req.Header.Add("X-Sumo-Category", s.sources.category.format(flds)) } switch pipeline { case LogsPipeline: req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("X-Sumo-Fields", string(flds)) + req.Header.Add("X-Sumo-Fields", flds.string()) case MetricsPipeline: // ToDo: Implement metrics pipeline return errors.New("current sender version doesn't support metrics") diff --git a/exporter/sumologicexporter/sender_test.go b/exporter/sumologicexporter/sender_test.go index 74a7f7d9a58a..65a62d83e70b 100644 --- a/exporter/sumologicexporter/sender_test.go +++ b/exporter/sumologicexporter/sender_test.go @@ -65,6 +65,11 @@ func prepareSenderTest(t *testing.T, cb []func(w http.ResponseWriter, req *http. Timeout: cfg.HTTPClientSettings.Timeout, }, f, + sourceFormats{ + host: getTestSourceFormat(t, "source_host"), + category: getTestSourceFormat(t, "source_category"), + name: getTestSourceFormat(t, "source_name"), + }, ), } } @@ -103,7 +108,7 @@ func TestSend(t *testing.T) { func(w http.ResponseWriter, req *http.Request) { body := extractBody(t, req) assert.Equal(t, "Example log\nAnother example log", body) - assert.Equal(t, "test_metadata", req.Header.Get("X-Sumo-Fields")) + assert.Equal(t, "key1=value, key2=value2", req.Header.Get("X-Sumo-Fields")) assert.Equal(t, "otelcol", req.Header.Get("X-Sumo-Client")) assert.Equal(t, "application/x-www-form-urlencoded", req.Header.Get("Content-Type")) }, @@ -112,7 +117,7 @@ func TestSend(t *testing.T) { test.s.buffer = exampleTwoLogs() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{"key1": "value", "key2": "value2"}) assert.NoError(t, err) } @@ -131,7 +136,7 @@ func TestSendSplit(t *testing.T) { test.s.config.MaxRequestBodySize = 10 test.s.buffer = exampleTwoLogs() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{}) assert.NoError(t, err) } func TestSendSplitFailedOne(t *testing.T) { @@ -152,7 +157,7 @@ func TestSendSplitFailedOne(t *testing.T) { test.s.config.LogFormat = TextFormat test.s.buffer = exampleTwoLogs() - dropped, err := test.s.sendLogs("test_metadata") + dropped, err := test.s.sendLogs(fields{}) assert.EqualError(t, err, "error during sending data: 500 Internal Server Error") assert.Equal(t, test.s.buffer[0:1], dropped) } @@ -177,7 +182,7 @@ func TestSendSplitFailedAll(t *testing.T) { test.s.config.LogFormat = TextFormat test.s.buffer = exampleTwoLogs() - dropped, err := test.s.sendLogs("test_metadata") + dropped, err := test.s.sendLogs(fields{}) assert.EqualError( t, err, @@ -193,7 +198,7 @@ func TestSendJson(t *testing.T) { expected := `{"key1":"value1","key2":"value2","log":"Example log"} {"key1":"value1","key2":"value2","log":"Another example log"}` assert.Equal(t, expected, body) - assert.Equal(t, "test_metadata", req.Header.Get("X-Sumo-Fields")) + assert.Equal(t, "key=value", req.Header.Get("X-Sumo-Fields")) assert.Equal(t, "otelcol", req.Header.Get("X-Sumo-Client")) assert.Equal(t, "application/x-www-form-urlencoded", req.Header.Get("Content-Type")) }, @@ -202,7 +207,7 @@ func TestSendJson(t *testing.T) { test.s.config.LogFormat = JSONFormat test.s.buffer = exampleTwoLogs() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{"key": "value"}) assert.NoError(t, err) } @@ -222,7 +227,7 @@ func TestSendJsonSplit(t *testing.T) { test.s.config.MaxRequestBodySize = 10 test.s.buffer = exampleTwoLogs() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{}) assert.NoError(t, err) } @@ -244,7 +249,7 @@ func TestSendJsonSplitFailedOne(t *testing.T) { test.s.config.MaxRequestBodySize = 10 test.s.buffer = exampleTwoLogs() - dropped, err := test.s.sendLogs("test_metadata") + dropped, err := test.s.sendLogs(fields{}) assert.EqualError(t, err, "error during sending data: 500 Internal Server Error") assert.Equal(t, test.s.buffer[0:1], dropped) } @@ -269,7 +274,7 @@ func TestSendJsonSplitFailedAll(t *testing.T) { test.s.config.MaxRequestBodySize = 10 test.s.buffer = exampleTwoLogs() - dropped, err := test.s.sendLogs("test_metadata") + dropped, err := test.s.sendLogs(fields{}) assert.EqualError( t, err, @@ -287,52 +292,52 @@ func TestSendUnexpectedFormat(t *testing.T) { test.s.config.LogFormat = "dummy" test.s.buffer = exampleTwoLogs() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{}) assert.Error(t, err) } func TestOverrideSourceName(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){ func(w http.ResponseWriter, req *http.Request) { - assert.Equal(t, "Test source name", req.Header.Get("X-Sumo-Name")) + assert.Equal(t, "Test source name/test_name", req.Header.Get("X-Sumo-Name")) }, }) defer func() { test.srv.Close() }() - test.s.config.SourceName = "Test source name" + test.s.sources.name = getTestSourceFormat(t, "Test source name/%{key1}") test.s.buffer = exampleLog() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{"key1": "test_name"}) assert.NoError(t, err) } func TestOverrideSourceCategory(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){ func(w http.ResponseWriter, req *http.Request) { - assert.Equal(t, "Test source category", req.Header.Get("X-Sumo-Category")) + assert.Equal(t, "Test source category/test_name", req.Header.Get("X-Sumo-Category")) }, }) defer func() { test.srv.Close() }() - test.s.config.SourceCategory = "Test source category" + test.s.sources.category = getTestSourceFormat(t, "Test source category/%{key1}") test.s.buffer = exampleLog() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{"key1": "test_name"}) assert.NoError(t, err) } func TestOverrideSourceHost(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){ func(w http.ResponseWriter, req *http.Request) { - assert.Equal(t, "Test source host", req.Header.Get("X-Sumo-Host")) + assert.Equal(t, "Test source host/test_name", req.Header.Get("X-Sumo-Host")) }, }) defer func() { test.srv.Close() }() - test.s.config.SourceHost = "Test source host" + test.s.sources.host = getTestSourceFormat(t, "Test source host/%{key1}") test.s.buffer = exampleLog() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{"key1": "test_name"}) assert.NoError(t, err) } @@ -343,13 +348,13 @@ func TestBuffer(t *testing.T) { assert.Equal(t, test.s.count(), 0) logs := exampleTwoLogs() - droppedLogs, err := test.s.batch(logs[0], "") + droppedLogs, err := test.s.batch(logs[0], fields{}) require.NoError(t, err) assert.Nil(t, droppedLogs) assert.Equal(t, 1, test.s.count()) assert.Equal(t, []pdata.LogRecord{logs[0]}, test.s.buffer) - droppedLogs, err = test.s.batch(logs[1], "") + droppedLogs, err = test.s.batch(logs[1], fields{}) require.NoError(t, err) assert.Nil(t, droppedLogs) assert.Equal(t, 2, test.s.count()) @@ -367,7 +372,7 @@ func TestInvalidEndpoint(t *testing.T) { test.s.config.HTTPClientSettings.Endpoint = ":" test.s.buffer = exampleLog() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{}) assert.EqualError(t, err, `parse ":": missing protocol scheme`) } @@ -378,7 +383,7 @@ func TestInvalidPostRequest(t *testing.T) { test.s.config.HTTPClientSettings.Endpoint = "" test.s.buffer = exampleLog() - _, err := test.s.sendLogs("test_metadata") + _, err := test.s.sendLogs(fields{}) assert.EqualError(t, err, `Post "": unsupported protocol scheme ""`) } @@ -390,11 +395,11 @@ func TestBufferOverflow(t *testing.T) { log := exampleLog() for test.s.count() < maxBufferSize-1 { - _, err := test.s.batch(log[0], "test_metadata") + _, err := test.s.batch(log[0], fields{}) require.NoError(t, err) } - _, err := test.s.batch(log[0], "test_metadata") + _, err := test.s.batch(log[0], fields{}) assert.EqualError(t, err, `parse ":": missing protocol scheme`) assert.Equal(t, 0, test.s.count()) } @@ -403,7 +408,7 @@ func TestMetricsPipeline(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){}) defer func() { test.srv.Close() }() - err := test.s.send(MetricsPipeline, strings.NewReader(""), "") + err := test.s.send(MetricsPipeline, strings.NewReader(""), fields{}) assert.EqualError(t, err, `current sender version doesn't support metrics`) } @@ -411,6 +416,6 @@ func TestInvalidPipeline(t *testing.T) { test := prepareSenderTest(t, []func(w http.ResponseWriter, req *http.Request){}) defer func() { test.srv.Close() }() - err := test.s.send("invalidPipeline", strings.NewReader(""), "") + err := test.s.send("invalidPipeline", strings.NewReader(""), fields{}) assert.EqualError(t, err, `unexpected pipeline`) } diff --git a/exporter/sumologicexporter/source_format.go b/exporter/sumologicexporter/source_format.go new file mode 100644 index 000000000000..2f9e1118f430 --- /dev/null +++ b/exporter/sumologicexporter/source_format.go @@ -0,0 +1,86 @@ +// Copyright 2020 OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sumologicexporter + +import ( + "fmt" + "regexp" +) + +type sourceFormats struct { + name sourceFormat + host sourceFormat + category sourceFormat +} + +type sourceFormat struct { + matches []string + template string +} + +const sourceRegex = `\%\{(\w+)\}` + +// newSourceFormat builds sourceFormat basing on the regex and given text. +// Regex is basing on the `sourceRegex` const +// For given example text: `%{cluster}/%{namespace}``, it sets: +// - template to `%s/%s`, which can be used later by fmt.Sprintf +// - matches as map of (attribute) keys ({"cluster", "namespace"}) which will +// be used to put corresponding value into templates' `%s +func newSourceFormat(r *regexp.Regexp, text string) sourceFormat { + matches := r.FindAllStringSubmatch(text, -1) + template := r.ReplaceAllString(text, "%s") + + m := make([]string, len(matches)) + + for i, match := range matches { + m[i] = match[1] + } + + return sourceFormat{ + matches: m, + template: template, + } +} + +// newSourceFormats returns sourceFormats for name, host and category based on cfg +func newSourceFormats(cfg *Config) (sourceFormats, error) { + r, err := regexp.Compile(sourceRegex) + if err != nil { + return sourceFormats{}, err + } + + return sourceFormats{ + category: newSourceFormat(r, cfg.SourceCategory), + host: newSourceFormat(r, cfg.SourceHost), + name: newSourceFormat(r, cfg.SourceName), + }, nil +} + +// format converts sourceFormat to string. +// Takes fields and put into template (%s placeholders) in order defined by matches +func (s *sourceFormat) format(f fields) string { + labels := make([]interface{}, 0, len(s.matches)) + + for _, matchset := range s.matches { + labels = append(labels, f[matchset]) + } + + return fmt.Sprintf(s.template, labels...) +} + +// isSet returns true if template is non-empty +func (s *sourceFormat) isSet() bool { + return len(s.template) > 0 +} diff --git a/exporter/sumologicexporter/source_format_test.go b/exporter/sumologicexporter/source_format_test.go new file mode 100644 index 000000000000..9533da0c027f --- /dev/null +++ b/exporter/sumologicexporter/source_format_test.go @@ -0,0 +1,108 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sumologicexporter + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func getTestSourceFormat(t *testing.T, template string) sourceFormat { + r, err := regexp.Compile(`\%\{(\w+)\}`) + require.NoError(t, err) + + return newSourceFormat(r, template) +} + +func TestNewSourceFormat(t *testing.T) { + expected := sourceFormat{ + matches: []string{ + "test", + }, + template: "%s/test", + } + + r, err := regexp.Compile(`\%\{(\w+)\}`) + require.NoError(t, err) + + s := newSourceFormat(r, "%{test}/test") + + assert.Equal(t, expected, s) +} + +func TestNewSourceFormats(t *testing.T) { + expected := sourceFormats{ + host: sourceFormat{ + matches: []string{ + "namespace", + }, + template: "ns/%s", + }, + name: sourceFormat{ + matches: []string{ + "pod", + }, + template: "name/%s", + }, + category: sourceFormat{ + matches: []string{ + "cluster", + }, + template: "category/%s", + }, + } + + cfg := &Config{ + SourceName: "name/%{pod}", + SourceHost: "ns/%{namespace}", + SourceCategory: "category/%{cluster}", + } + + s, err := newSourceFormats(cfg) + require.NoError(t, err) + + assert.Equal(t, expected, s) +} + +func TestFormat(t *testing.T) { + f := fields{"key_1": "value_1", "key_2": "value_2"} + s := getTestSourceFormat(t, "%{key_1}/%{key_2}") + expected := "value_1/value_2" + + result := s.format(f) + assert.Equal(t, expected, result) +} + +func TestFormatNonExistingKey(t *testing.T) { + f := fields{"key_2": "value_2"} + s := getTestSourceFormat(t, "%{key_1}/%{key_2}") + expected := "/value_2" + + result := s.format(f) + assert.Equal(t, expected, result) +} + +func TestIsSet(t *testing.T) { + s := getTestSourceFormat(t, "%{key_1}/%{key_2}") + assert.True(t, s.isSet()) +} + +func TestIsNotSet(t *testing.T) { + s := getTestSourceFormat(t, "") + assert.False(t, s.isSet()) +}