Skip to content

Commit

Permalink
Fix empty log body printed by stdoutlog exporter (#5311)
Browse files Browse the repository at this point in the history
  • Loading branch information
XSAM committed May 9, 2024
1 parent 9e7d744 commit 9f1de84
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The `go.opentelemetry.io/otel/exporters/stdout/stdoutlog` exporter won't print `AttributeValueLengthLimit` and `AttributeCountLimit` fields now, instead it prints the `DroppedAttributes` field. (#5272)
- Improved performance in the `Stringer` implementation of `go.opentelemetry.io/otel/baggage.Member` by reducing the number of allocations. (#5286)

### Fixed

- Fix the empty output of `go.opentelemetry.io/otel/log.Value` in `go.opentelemetry.io/otel/exporters/stdout/stdoutlog`. (#5311)

## [1.26.0/0.48.0/0.2.0-alpha] 2024-04-24

### Added
Expand Down
118 changes: 110 additions & 8 deletions exporters/stdout/stdoutlog/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func getJSON(now *time.Time) string {
timestamps = "\"Timestamp\":" + string(serializedNow) + ",\"ObservedTimestamp\":" + string(serializedNow) + ","
}

return "{" + timestamps + "\"Severity\":9,\"SeverityText\":\"INFO\",\"Body\":{},\"Attributes\":[{\"Key\":\"key\",\"Value\":{}},{\"Key\":\"key2\",\"Value\":{}},{\"Key\":\"key3\",\"Value\":{}},{\"Key\":\"key4\",\"Value\":{}},{\"Key\":\"key5\",\"Value\":{}},{\"Key\":\"bool\",\"Value\":{}}],\"TraceID\":\"0102030405060708090a0b0c0d0e0f10\",\"SpanID\":\"0102030405060708\",\"TraceFlags\":\"01\",\"Resource\":[{\"Key\":\"foo\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"bar\"}}],\"Scope\":{\"Name\":\"name\",\"Version\":\"version\",\"SchemaURL\":\"https://example.com/custom-schema\"},\"DroppedAttributes\":10}\n"
return "{" + timestamps + "\"Severity\":9,\"SeverityText\":\"INFO\",\"Body\":{\"Type\":\"String\",\"Value\":\"test\"},\"Attributes\":[{\"Key\":\"key\",\"Value\":{\"Type\":\"String\",\"Value\":\"value\"}},{\"Key\":\"key2\",\"Value\":{\"Type\":\"String\",\"Value\":\"value\"}},{\"Key\":\"key3\",\"Value\":{\"Type\":\"String\",\"Value\":\"value\"}},{\"Key\":\"key4\",\"Value\":{\"Type\":\"String\",\"Value\":\"value\"}},{\"Key\":\"key5\",\"Value\":{\"Type\":\"String\",\"Value\":\"value\"}},{\"Key\":\"bool\",\"Value\":{\"Type\":\"Bool\",\"Value\":true}}],\"TraceID\":\"0102030405060708090a0b0c0d0e0f10\",\"SpanID\":\"0102030405060708\",\"TraceFlags\":\"01\",\"Resource\":[{\"Key\":\"foo\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"bar\"}}],\"Scope\":{\"Name\":\"name\",\"Version\":\"version\",\"SchemaURL\":\"https://example.com/custom-schema\"},\"DroppedAttributes\":10}\n"
}

func getJSONs(now *time.Time) string {
Expand All @@ -200,31 +200,52 @@ func getPrettyJSON(now *time.Time) string {
return `{` + timestamps + `
"Severity": 9,
"SeverityText": "INFO",
"Body": {},
"Body": {
"Type": "String",
"Value": "test"
},
"Attributes": [
{
"Key": "key",
"Value": {}
"Value": {
"Type": "String",
"Value": "value"
}
},
{
"Key": "key2",
"Value": {}
"Value": {
"Type": "String",
"Value": "value"
}
},
{
"Key": "key3",
"Value": {}
"Value": {
"Type": "String",
"Value": "value"
}
},
{
"Key": "key4",
"Value": {}
"Value": {
"Type": "String",
"Value": "value"
}
},
{
"Key": "key5",
"Value": {}
"Value": {
"Type": "String",
"Value": "value"
}
},
{
"Key": "bool",
"Value": {}
"Value": {
"Type": "Bool",
"Value": true
}
}
],
"TraceID": "0102030405060708090a0b0c0d0e0f10",
Expand Down Expand Up @@ -344,3 +365,84 @@ func TestExporterConcurrentSafe(t *testing.T) {
})
}
}

func TestValueMarshalJSON(t *testing.T) {
testCases := []struct {
value log.Value
want string
}{
{
value: log.Empty("test").Value,
want: `{"Type":"Empty","Value":null}`,
},
{
value: log.BoolValue(true),
want: `{"Type":"Bool","Value":true}`,
},
{
value: log.Float64Value(3.14),
want: `{"Type":"Float64","Value":3.14}`,
},
{
value: log.Int64Value(42),
want: `{"Type":"Int64","Value":42}`,
},
{
value: log.StringValue("hello"),
want: `{"Type":"String","Value":"hello"}`,
},
{
value: log.BytesValue([]byte{1, 2, 3}),
// The base64 encoding of []byte{1, 2, 3} is "AQID".
want: `{"Type":"Bytes","Value":"AQID"}`,
},
{
value: log.SliceValue(
log.Empty("empty").Value,
log.BoolValue(true),
log.Float64Value(2.2),
log.IntValue(3),
log.StringValue("4"),
log.BytesValue([]byte{5}),
log.SliceValue(
log.IntValue(6),
log.MapValue(
log.Int("seven", 7),
),
),
log.MapValue(
log.Int("nine", 9),
),
),
want: `{"Type":"Slice","Value":[{"Type":"Empty","Value":null},{"Type":"Bool","Value":true},{"Type":"Float64","Value":2.2},{"Type":"Int64","Value":3},{"Type":"String","Value":"4"},{"Type":"Bytes","Value":"BQ=="},{"Type":"Slice","Value":[{"Type":"Int64","Value":6},{"Type":"Map","Value":[{"Key":"seven","Value":{"Type":"Int64","Value":7}}]}]},{"Type":"Map","Value":[{"Key":"nine","Value":{"Type":"Int64","Value":9}}]}]}`,
},
{
value: log.MapValue(
log.Empty("empty"),
log.Bool("one", true),
log.Float64("two", 2.2),
log.Int("three", 3),
log.String("four", "4"),
log.Bytes("five", []byte{5}),
log.Slice("six",
log.IntValue(6),
log.MapValue(
log.Int("seven", 7),
),
),
log.Map("eight",
log.Int("nine", 9),
),
),
want: `{"Type":"Map","Value":[{"Key":"empty","Value":{"Type":"Empty","Value":null}},{"Key":"one","Value":{"Type":"Bool","Value":true}},{"Key":"two","Value":{"Type":"Float64","Value":2.2}},{"Key":"three","Value":{"Type":"Int64","Value":3}},{"Key":"four","Value":{"Type":"String","Value":"4"}},{"Key":"five","Value":{"Type":"Bytes","Value":"BQ=="}},{"Key":"six","Value":{"Type":"Slice","Value":[{"Type":"Int64","Value":6},{"Type":"Map","Value":[{"Key":"seven","Value":{"Type":"Int64","Value":7}}]}]}},{"Key":"eight","Value":{"Type":"Map","Value":[{"Key":"nine","Value":{"Type":"Int64","Value":9}}]}}]}`,
},
}

for _, tc := range testCases {
t.Run(tc.value.String(), func(t *testing.T) {
got, err := json.Marshal(value{Value: tc.value})
require.NoError(t, err)
assert.JSONEq(t, tc.want, string(got))
})
}
}
75 changes: 70 additions & 5 deletions exporters/stdout/stdoutlog/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog"

import (
"encoding/json"
"errors"
"time"

"go.opentelemetry.io/otel/log"
Expand All @@ -13,14 +15,74 @@ import (
"go.opentelemetry.io/otel/trace"
)

func newValue(v log.Value) value {
return value{Value: v}
}

type value struct {
log.Value
}

// MarshalJSON implements a custom marshal function to encode log.Value.
func (v value) MarshalJSON() ([]byte, error) {
var jsonVal struct {
Type string
Value interface{}
}
jsonVal.Type = v.Kind().String()

switch v.Kind() {
case log.KindString:
jsonVal.Value = v.AsString()
case log.KindInt64:
jsonVal.Value = v.AsInt64()
case log.KindFloat64:
jsonVal.Value = v.AsFloat64()
case log.KindBool:
jsonVal.Value = v.AsBool()
case log.KindBytes:
jsonVal.Value = v.AsBytes()
case log.KindMap:
m := v.AsMap()
values := make([]keyValue, 0, len(m))
for _, kv := range m {
values = append(values, keyValue{
Key: kv.Key,
Value: newValue(kv.Value),
})
}

jsonVal.Value = values
case log.KindSlice:
s := v.AsSlice()
values := make([]value, 0, len(s))
for _, e := range s {
values = append(values, newValue(e))
}

jsonVal.Value = values
case log.KindEmpty:
jsonVal.Value = nil
default:
return nil, errors.New("invalid Kind")
}

return json.Marshal(jsonVal)
}

type keyValue struct {
Key string
Value value
}

// recordJSON is a JSON-serializable representation of a Record.
type recordJSON struct {
Timestamp *time.Time `json:",omitempty"`
ObservedTimestamp *time.Time `json:",omitempty"`
Severity log.Severity
SeverityText string
Body log.Value
Attributes []log.KeyValue
Body value
Attributes []keyValue
TraceID trace.TraceID
SpanID trace.SpanID
TraceFlags trace.TraceFlags
Expand All @@ -34,13 +96,13 @@ func (e *Exporter) newRecordJSON(r sdklog.Record) recordJSON {
newRecord := recordJSON{
Severity: r.Severity(),
SeverityText: r.SeverityText(),
Body: r.Body(),
Body: newValue(r.Body()),

TraceID: r.TraceID(),
SpanID: r.SpanID(),
TraceFlags: r.TraceFlags(),

Attributes: make([]log.KeyValue, 0, r.AttributesLen()),
Attributes: make([]keyValue, 0, r.AttributesLen()),

Resource: &res,
Scope: r.InstrumentationScope(),
Expand All @@ -49,7 +111,10 @@ func (e *Exporter) newRecordJSON(r sdklog.Record) recordJSON {
}

r.WalkAttributes(func(kv log.KeyValue) bool {
newRecord.Attributes = append(newRecord.Attributes, kv)
newRecord.Attributes = append(newRecord.Attributes, keyValue{
Key: kv.Key,
Value: newValue(kv.Value),
})
return true
})

Expand Down

0 comments on commit 9f1de84

Please sign in to comment.