Skip to content

Commit bbab39d

Browse files
authored
sumologicexporter: add graphite format (#2695)
- Add support for graphite format to sumo logic exporter - Add sanitization for carbon2 format - Replace `=` with `:` for sanitization **Link to tracking Issue:** #1498
1 parent e9f085b commit bbab39d

File tree

12 files changed

+417
-11
lines changed

12 files changed

+417
-11
lines changed

exporter/sumologicexporter/README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ Empty string means no compression
1010
- `max_request_body_size` (optional): Max HTTP request body size in bytes before compression (if applied). By default `1_048_576` (1MB) is used.
1111
- `metadata_attributes` (optional): List of regexes for attributes which should be send as metadata
1212
- `log_format` (optional) (logs only): Format to use when sending logs to Sumo. (default `json`) (possible values: `json`, `text`)
13-
- `metric_format` (optional) (metrics only): Format of the metrics to be sent (default is `prometheus`) (possible values: `carbon2`, `prometheus`)
14-
`carbon2` and `graphite` are going to be supported soon.
13+
- `metric_format` (optional) (metrics only): Format of the metrics to be sent (default is `prometheus`) (possible values: `carbon2`, `graphite`, `prometheus`).
14+
- `graphite_template` (default=`%{_metric_}`) (optional) (metrics only): Template for Graphite format.
15+
[Source templates](#source-templates) are going to be applied.
16+
Applied only if `metric_format` is set to `graphite`.
1517
- `source_category` (optional): Desired source category. Useful if you want to override the source category configured for the source.
18+
[Source templates](#source-templates) are going to be applied.
1619
- `source_name` (optional): Desired source name. Useful if you want to override the source name configured for the source.
20+
[Source templates](#source-templates) are going to be applied.
1721
- `source_host` (optional): Desired host name. Useful if you want to override the source host configured for the source.
22+
[Source templates](#source-templates) are going to be applied.
1823
- `timeout` (default = 5s): Is the timeout for every attempt to send data to the backend.
1924
Maximum connection timeout is 55s.
2025
- `retry_on_failure`
@@ -30,7 +35,15 @@ Maximum connection timeout is 55s.
3035
- `num_seconds` is the number of seconds to buffer in case of a backend outage
3136
- `requests_per_second` is the average number of requests per seconds.
3237

33-
Example:
38+
## Source Templates
39+
40+
You can specify a template with an attribute for `source_category`, `source_name`, `source_host` or `graphite_template` using `%{attr_name}`.
41+
42+
For example, when there is an attribute `my_attr`: `my_value`, `metrics/%{my_attr}` would be expanded to `metrics/my_value`.
43+
44+
For `graphite_template`, in addition to above, `%{_metric_}` is going to be replaced with metric name.
45+
46+
## Example Configuration
3447

3548
```yaml
3649
exporters:

exporter/sumologicexporter/carbon_formatter.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,27 @@ func carbon2TagString(record metricPair) string {
4242
if k == "name" || k == "unit" {
4343
k = fmt.Sprintf("_%s", k)
4444
}
45-
returnValue = append(returnValue, fmt.Sprintf("%s=%s", k, tracetranslator.AttributeValueToString(v, false)))
45+
returnValue = append(returnValue, fmt.Sprintf(
46+
"%s=%s",
47+
sanitizeCarbonString(k),
48+
sanitizeCarbonString(tracetranslator.AttributeValueToString(v, false)),
49+
))
4650
})
4751

48-
returnValue = append(returnValue, fmt.Sprintf("metric=%s", record.metric.Name()))
52+
returnValue = append(returnValue, fmt.Sprintf("metric=%s", sanitizeCarbonString(record.metric.Name())))
4953

5054
if len(record.metric.Unit()) > 0 {
51-
returnValue = append(returnValue, fmt.Sprintf("unit=%s", record.metric.Unit()))
55+
returnValue = append(returnValue, fmt.Sprintf("unit=%s", sanitizeCarbonString(record.metric.Unit())))
5256
}
5357

5458
return strings.Join(returnValue, " ")
5559
}
5660

61+
// sanitizeCarbonString replaces problematic characters with underscore
62+
func sanitizeCarbonString(text string) string {
63+
return strings.NewReplacer(" ", "_", "=", ":", "\n", "_").Replace(text)
64+
}
65+
5766
// carbon2IntRecord converts IntDataPoint to carbon2 metric string
5867
// with additional information from metricPair.
5968
func carbon2IntRecord(record metricPair, dataPoint pdata.IntDataPoint) string {

exporter/sumologicexporter/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,25 @@ type Config struct {
4646
// The format of metrics you will be sending, either graphite or carbon2 or prometheus (Default is prometheus)
4747
// Possible values are `carbon2` and `prometheus`
4848
MetricFormat MetricFormatType `mapstructure:"metric_format"`
49+
// Graphite template.
50+
// Placeholders `%{attr_name}` will be replaced with attribute value for attr_name.
51+
GraphiteTemplate string `mapstructure:"graphite_template"`
4952

5053
// List of regexes for attributes which should be send as metadata
5154
MetadataAttributes []string `mapstructure:"metadata_attributes"`
5255

5356
// Sumo specific options
5457
// Desired source category.
5558
// Useful if you want to override the source category configured for the source.
59+
// Placeholders `%{attr_name}` will be replaced with attribute value for attr_name.
5660
SourceCategory string `mapstructure:"source_category"`
5761
// Desired source name.
5862
// Useful if you want to override the source name configured for the source.
63+
// Placeholders `%{attr_name}` will be replaced with attribute value for attr_name.
5964
SourceName string `mapstructure:"source_name"`
6065
// Desired host name.
6166
// Useful if you want to override the source host configured for the source.
67+
// Placeholders `%{attr_name}` will be replaced with attribute value for attr_name.
6268
SourceHost string `mapstructure:"source_host"`
6369
// Name of the client
6470
Client string `mapstructure:"client"`
@@ -124,4 +130,6 @@ const (
124130
DefaultSourceHost string = ""
125131
// DefaultClient defines default Client
126132
DefaultClient string = "otelcol"
133+
// DefaultGraphiteTemplate defines default template for Graphite
134+
DefaultGraphiteTemplate string = "%{_metric_}"
127135
)

exporter/sumologicexporter/exporter.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type sumologicexporter struct {
3232
client *http.Client
3333
filter filter
3434
prometheusFormatter prometheusFormatter
35+
graphiteFormatter graphiteFormatter
3536
}
3637

3738
func initExporter(cfg *Config) (*sumologicexporter, error) {
@@ -77,6 +78,11 @@ func initExporter(cfg *Config) (*sumologicexporter, error) {
7778
return nil, err
7879
}
7980

81+
gf, err := newGraphiteFormatter(cfg.GraphiteTemplate)
82+
if err != nil {
83+
return nil, err
84+
}
85+
8086
httpClient, err := cfg.HTTPClientSettings.ToClient()
8187
if err != nil {
8288
return nil, fmt.Errorf("failed to create HTTP Client: %w", err)
@@ -88,6 +94,7 @@ func initExporter(cfg *Config) (*sumologicexporter, error) {
8894
client: httpClient,
8995
filter: f,
9096
prometheusFormatter: pf,
97+
graphiteFormatter: gf,
9198
}
9299

93100
return se, nil
@@ -151,7 +158,15 @@ func (se *sumologicexporter) pushLogsData(ctx context.Context, ld pdata.Logs) er
151158
if err != nil {
152159
return consumererror.NewLogs(fmt.Errorf("failed to initialize compressor: %w", err), ld)
153160
}
154-
sdr := newSender(se.config, se.client, se.filter, se.sources, c, se.prometheusFormatter)
161+
sdr := newSender(
162+
se.config,
163+
se.client,
164+
se.filter,
165+
se.sources,
166+
c,
167+
se.prometheusFormatter,
168+
se.graphiteFormatter,
169+
)
155170

156171
// Iterate over ResourceLogs
157172
rls := ld.ResourceLogs()
@@ -246,7 +261,15 @@ func (se *sumologicexporter) pushMetricsData(ctx context.Context, md pdata.Metri
246261
if err != nil {
247262
return consumererror.NewMetrics(fmt.Errorf("failed to initialize compressor: %w", err), md)
248263
}
249-
sdr := newSender(se.config, se.client, se.filter, se.sources, c, se.prometheusFormatter)
264+
sdr := newSender(
265+
se.config,
266+
se.client,
267+
se.filter,
268+
se.sources,
269+
c,
270+
se.prometheusFormatter,
271+
se.graphiteFormatter,
272+
)
250273

251274
// Iterate over ResourceMetrics
252275
rms := md.ResourceMetrics()

exporter/sumologicexporter/factory.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func createDefaultConfig() config.Exporter {
5353
SourceName: DefaultSourceName,
5454
SourceHost: DefaultSourceHost,
5555
Client: DefaultClient,
56+
GraphiteTemplate: DefaultGraphiteTemplate,
5657

5758
HTTPClientSettings: CreateDefaultHTTPClientSettings(),
5859
RetrySettings: exporterhelper.DefaultRetrySettings(),

exporter/sumologicexporter/factory_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func TestCreateDefaultConfig(t *testing.T) {
4747
SourceName: "",
4848
SourceHost: "",
4949
Client: "otelcol",
50+
GraphiteTemplate: "%{_metric_}",
5051

5152
HTTPClientSettings: confighttp.HTTPClientSettings{
5253
Timeout: 5 * time.Second,

exporter/sumologicexporter/fields.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type fields struct {
3232
func newFields(attrMap pdata.AttributeMap) fields {
3333
return fields{
3434
orig: attrMap,
35-
replacer: strings.NewReplacer(",", "_", "=", "_", "\n", "_"),
35+
replacer: strings.NewReplacer(",", "_", "=", ":", "\n", "_"),
3636
}
3737
}
3838

exporter/sumologicexporter/fields_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestFieldsAsString(t *testing.T) {
3232
}
3333

3434
func TestFieldsSanitization(t *testing.T) {
35-
expected := "key1=value_1, key3=value_3, key__2=valu_e_2"
35+
expected := "key1=value_1, key3=value_3, key:_2=valu_e:2"
3636
flds := fieldsFromMap(map[string]string{
3737
"key1": "value,1",
3838
"key3": "value\n3",
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2021, OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sumologicexporter
16+
17+
import (
18+
"fmt"
19+
"regexp"
20+
"strings"
21+
"time"
22+
23+
"go.opentelemetry.io/collector/consumer/pdata"
24+
tracetranslator "go.opentelemetry.io/collector/translator/trace"
25+
)
26+
27+
type graphiteFormatter struct {
28+
template sourceFormat
29+
replacer *strings.Replacer
30+
}
31+
32+
const (
33+
graphiteMetricNamePlaceholder = "_metric_"
34+
)
35+
36+
// newGraphiteFormatter creates new formatter for given SourceFormat template
37+
func newGraphiteFormatter(template string) (graphiteFormatter, error) {
38+
r, err := regexp.Compile(sourceRegex)
39+
if err != nil {
40+
return graphiteFormatter{}, err
41+
}
42+
43+
sf := newSourceFormat(r, template)
44+
45+
return graphiteFormatter{
46+
template: sf,
47+
replacer: strings.NewReplacer(`.`, `_`, ` `, `_`),
48+
}, nil
49+
}
50+
51+
// escapeGraphiteString replaces dot and space using replacer,
52+
// as dot is special character for graphite format
53+
func (gf *graphiteFormatter) escapeGraphiteString(value string) string {
54+
return gf.replacer.Replace(value)
55+
}
56+
57+
// format returns metric name basing on template for given fields nas metric name
58+
func (gf *graphiteFormatter) format(f fields, metricName string) string {
59+
s := gf.template
60+
labels := make([]interface{}, 0, len(s.matches))
61+
62+
for _, matchset := range s.matches {
63+
if matchset == graphiteMetricNamePlaceholder {
64+
labels = append(labels, gf.escapeGraphiteString(metricName))
65+
} else {
66+
attr, ok := f.orig.Get(matchset)
67+
var value string
68+
if ok {
69+
value = tracetranslator.AttributeValueToString(attr, false)
70+
} else {
71+
value = ""
72+
}
73+
labels = append(labels, gf.escapeGraphiteString(value))
74+
}
75+
}
76+
77+
return fmt.Sprintf(s.template, labels...)
78+
}
79+
80+
// intRecord converts IntDataPoint to graphite metric string
81+
// with additional information from fields
82+
func (gf *graphiteFormatter) intRecord(fs fields, name string, dataPoint pdata.IntDataPoint) string {
83+
return fmt.Sprintf("%s %d %d",
84+
gf.format(fs, name),
85+
dataPoint.Value(),
86+
dataPoint.Timestamp()/pdata.Timestamp(time.Second),
87+
)
88+
}
89+
90+
// doubleRecord converts DoubleDataPoint to graphite metric string
91+
// with additional information from fields
92+
func (gf *graphiteFormatter) doubleRecord(fs fields, name string, dataPoint pdata.DoubleDataPoint) string {
93+
return fmt.Sprintf("%s %g %d",
94+
gf.format(fs, name),
95+
dataPoint.Value(),
96+
dataPoint.Timestamp()/pdata.Timestamp(time.Second),
97+
)
98+
}
99+
100+
// metric2String returns stringified metricPair
101+
func (gf *graphiteFormatter) metric2String(record metricPair) string {
102+
var nextLines []string
103+
fs := newFields(record.attributes)
104+
name := record.metric.Name()
105+
106+
switch record.metric.DataType() {
107+
case pdata.MetricDataTypeIntGauge:
108+
dps := record.metric.IntGauge().DataPoints()
109+
nextLines = make([]string, 0, dps.Len())
110+
for i := 0; i < dps.Len(); i++ {
111+
nextLines = append(nextLines, gf.intRecord(fs, name, dps.At(i)))
112+
}
113+
case pdata.MetricDataTypeIntSum:
114+
dps := record.metric.IntSum().DataPoints()
115+
nextLines = make([]string, 0, dps.Len())
116+
for i := 0; i < dps.Len(); i++ {
117+
nextLines = append(nextLines, gf.intRecord(fs, name, dps.At(i)))
118+
}
119+
case pdata.MetricDataTypeDoubleGauge:
120+
dps := record.metric.DoubleGauge().DataPoints()
121+
nextLines = make([]string, 0, dps.Len())
122+
for i := 0; i < dps.Len(); i++ {
123+
nextLines = append(nextLines, gf.doubleRecord(fs, name, dps.At(i)))
124+
}
125+
case pdata.MetricDataTypeDoubleSum:
126+
dps := record.metric.DoubleSum().DataPoints()
127+
nextLines = make([]string, 0, dps.Len())
128+
for i := 0; i < dps.Len(); i++ {
129+
nextLines = append(nextLines, gf.doubleRecord(fs, name, dps.At(i)))
130+
}
131+
// Skip complex metrics
132+
case pdata.MetricDataTypeDoubleHistogram:
133+
case pdata.MetricDataTypeIntHistogram:
134+
case pdata.MetricDataTypeDoubleSummary:
135+
}
136+
137+
return strings.Join(nextLines, "\n")
138+
}

0 commit comments

Comments
 (0)