diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db3cf6ce6f..7251704c249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/exporters/stdout/stdoutlog/exporter_test.go b/exporters/stdout/stdoutlog/exporter_test.go index 606fba40b3f..659f7f462d7 100644 --- a/exporters/stdout/stdoutlog/exporter_test.go +++ b/exporters/stdout/stdoutlog/exporter_test.go @@ -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 { @@ -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", @@ -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)) + }) + } +} diff --git a/exporters/stdout/stdoutlog/record.go b/exporters/stdout/stdoutlog/record.go index 31a511dc15a..71512e23f93 100644 --- a/exporters/stdout/stdoutlog/record.go +++ b/exporters/stdout/stdoutlog/record.go @@ -4,6 +4,8 @@ package stdoutlog // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" import ( + "encoding/json" + "errors" "time" "go.opentelemetry.io/otel/log" @@ -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 @@ -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(), @@ -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 })