-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Loki Exporter - Adding a feature for loki exporter to encode JSON for log entry #5846
Changes from 22 commits
491e6e1
18d1808
403b6a1
4c695b7
caa4507
071a13c
bb37818
728f4bf
bf30cc5
bb253b3
11d5439
fcdb38b
e01ed0f
ebdcccb
45a52eb
e80de58
3d9ef4f
fc0c481
3139761
7fd92f5
a855093
30edcbe
0edc8fb
42d664c
f2f50d6
7e7fa2f
3d11fb3
119a6a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Copyright The 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 lokiexporter | ||
|
||
import ( | ||
"encoding/json" | ||
|
||
"go.opentelemetry.io/collector/model/pdata" | ||
) | ||
|
||
// JSON representation of the LogRecord as described by https://developers.google.com/protocol-buffers/docs/proto3#json | ||
|
||
type lokiEntry struct { | ||
Name string `json:"name,omitempty"` | ||
Body string `json:"body,omitempty"` | ||
TraceID string `json:"traceid,omitempty"` | ||
SpanID string `json:"spanid,omitempty"` | ||
Severity string `json:"severity,omitempty"` | ||
Attributes map[string]interface{} `json:"attributes,omitempty"` | ||
Resources map[string]interface{} `json:"resources,omitempty"` | ||
} | ||
|
||
func encodeJSON(lr pdata.LogRecord, res pdata.Resource) (string, error) { | ||
var logRecord lokiEntry | ||
var jsonRecord []byte | ||
|
||
logRecord = lokiEntry{ | ||
Name: lr.Name(), | ||
Body: lr.Body().StringVal(), | ||
TraceID: lr.TraceID().HexString(), | ||
SpanID: lr.SpanID().HexString(), | ||
Severity: lr.SeverityText(), | ||
Attributes: lr.Attributes().AsRaw(), | ||
Resources: res.Attributes().AsRaw(), | ||
} | ||
|
||
jsonRecord, err := json.Marshal(logRecord) | ||
if err != nil { | ||
return "", err | ||
} | ||
return string(jsonRecord), nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// Copyright The 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 lokiexporter | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"go.opentelemetry.io/collector/model/pdata" | ||
) | ||
|
||
func exampleLog() (pdata.LogRecord, pdata.Resource) { | ||
|
||
buffer := pdata.NewLogRecord() | ||
buffer.Body().SetStringVal("Example log") | ||
buffer.SetName("name") | ||
buffer.SetSeverityText("error") | ||
buffer.Attributes().Insert("attr1", pdata.NewAttributeValueString("1")) | ||
buffer.Attributes().Insert("attr2", pdata.NewAttributeValueString("2")) | ||
buffer.SetTraceID(pdata.NewTraceID([16]byte{1, 2, 3, 4})) | ||
buffer.SetSpanID(pdata.NewSpanID([8]byte{5, 6, 7, 8})) | ||
|
||
resource := pdata.NewResource() | ||
resource.Attributes().Insert("host.name", pdata.NewAttributeValueString("something")) | ||
|
||
return buffer, resource | ||
} | ||
|
||
func exampleJSON() string { | ||
jsonExample := `{"name":"name","body":"Example log","traceid":"01020304000000000000000000000000","spanid":"0506070800000000","severity":"error","attributes":{"attr1":"1","attr2":"2"},"resources":{"host.name":"something"}}` | ||
return jsonExample | ||
} | ||
|
||
func TestConvert(t *testing.T) { | ||
in := exampleJSON() | ||
out, err := encodeJSON(exampleLog()) | ||
t.Log(in) | ||
t.Log(out, err) | ||
assert.Equal(t, in, out) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ package lokiexporter | |
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
|
@@ -30,23 +31,31 @@ import ( | |
"go.opentelemetry.io/collector/component" | ||
"go.opentelemetry.io/collector/consumer/consumererror" | ||
"go.opentelemetry.io/collector/model/pdata" | ||
"go.uber.org/multierr" | ||
"go.uber.org/zap" | ||
|
||
"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/lokiexporter/internal/third_party/loki/logproto" | ||
) | ||
|
||
type lokiExporter struct { | ||
config *Config | ||
logger *zap.Logger | ||
client *http.Client | ||
wg sync.WaitGroup | ||
config *Config | ||
logger *zap.Logger | ||
client *http.Client | ||
wg sync.WaitGroup | ||
convert func(pdata.LogRecord, pdata.Resource) (*logproto.Entry, error) | ||
} | ||
|
||
func newExporter(config *Config, logger *zap.Logger) *lokiExporter { | ||
return &lokiExporter{ | ||
lokiexporter := &lokiExporter{ | ||
config: config, | ||
logger: logger, | ||
} | ||
if config.Format == "json" { | ||
lokiexporter.convert = convertLogToJSONEntry | ||
} else { | ||
lokiexporter.convert = convertLogBodyToEntry | ||
} | ||
return lokiexporter | ||
} | ||
|
||
func (l *lokiExporter) pushLogData(ctx context.Context, ld pdata.Logs) error { | ||
|
@@ -119,6 +128,8 @@ func (l *lokiExporter) stop(context.Context) (err error) { | |
} | ||
|
||
func (l *lokiExporter) logDataToLoki(ld pdata.Logs) (pr *logproto.PushRequest, numDroppedLogs int) { | ||
var errs error | ||
|
||
streams := make(map[string]*logproto.Stream) | ||
rls := ld.ResourceLogs() | ||
for i := 0; i < rls.Len(); i++ { | ||
|
@@ -135,7 +146,24 @@ func (l *lokiExporter) logDataToLoki(ld pdata.Logs) (pr *logproto.PushRequest, n | |
continue | ||
} | ||
labels := mergedLabels.String() | ||
entry := convertLogToLokiEntry(log) | ||
var entry *logproto.Entry | ||
var err error | ||
entry, err = l.convert(log, resource) | ||
if err != nil { | ||
// Couldn't convert so dropping log. | ||
numDroppedLogs++ | ||
errs = multierr.Append( | ||
errs, | ||
errors.New( | ||
fmt.Sprint( | ||
"failed to convert, dropping log", | ||
zap.String("format", l.config.Format), | ||
zap.Error(err), | ||
), | ||
), | ||
) | ||
continue | ||
} | ||
|
||
if stream, ok := streams[labels]; ok { | ||
stream.Entries = append(stream.Entries, *entry) | ||
|
@@ -150,6 +178,8 @@ func (l *lokiExporter) logDataToLoki(ld pdata.Logs) (pr *logproto.PushRequest, n | |
} | ||
} | ||
|
||
l.logger.Error("some logs has been dropped", zap.Error(errs)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be lost here. Shouldn't you wrap it in a conditional? |
||
|
||
pr = &logproto.PushRequest{ | ||
Streams: make([]logproto.Stream, len(streams)), | ||
} | ||
|
@@ -195,9 +225,20 @@ func (l *lokiExporter) convertAttributesToLabels(attributes pdata.AttributeMap, | |
return ls | ||
} | ||
|
||
func convertLogToLokiEntry(lr pdata.LogRecord) *logproto.Entry { | ||
func convertLogBodyToEntry(lr pdata.LogRecord, res pdata.Resource) (*logproto.Entry, error) { | ||
return &logproto.Entry{ | ||
Timestamp: time.Unix(0, int64(lr.Timestamp())), | ||
Line: lr.Body().StringVal(), | ||
}, nil | ||
} | ||
|
||
func convertLogToJSONEntry(lr pdata.LogRecord, res pdata.Resource) (*logproto.Entry, error) { | ||
line, err := encodeJSON(lr, res) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &logproto.Entry{ | ||
Timestamp: time.Unix(0, int64(lr.Timestamp())), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious about what is the behavior when the timestamp is bigger than the upper boundary for |
||
Line: line, | ||
}, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you actually need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but was to be consistent with the previous exemple. We can remove it if you think it's better.
Does memory ballast shouldn't always be used when we receive pushed data like logs ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We recommend that people always use things like the batch processor and memory ballast, but I find it confusing to add something not relevant to what is being demonstrated.