Skip to content
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

Fix empty log body printed by stdoutlog exporter #5311

Merged
merged 9 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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")

Check warning on line 67 in exporters/stdout/stdoutlog/record.go

View check run for this annotation

Codecov / codecov/patch

exporters/stdout/stdoutlog/record.go#L66-L67

Added lines #L66 - L67 were not covered by tests
}

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 @@
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 @@
}

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