Files
Ali AsgharandGitHub a0993a8dcf [pdata] don't HTML-escape ValueTypeMap/Slice AsString output (#15108)
## Description

`pcommon.Value.AsString` uses `json.Marshal` for `ValueTypeMap` and
`ValueTypeSlice`, which HTML-escapes `<`, `>`, and `&` into `\u003c`,
`\u003e`, and `\u0026`. This is inconsistent with `ValueTypeStr`, which
returns the raw string, and corrupts structured log bodies whose content
happens to contain those characters.

```
"<".AsString()       -> "<"
["<"].AsString()     -> ["\u003c"]   // surprising
{"key":"<"}.AsString() -> {"key":"\u003c"} // surprising
```

Switch to `json.Encoder` with `SetEscapeHTML(false)` so that maps and
slices serialize the same way strings do.

Closes #14662

## Link to tracking Issue
#14662

## Testing

Added `map_with_html_characters` and `slice_with_html_characters`
sub-tests to `TestValueAsString`. `go test ./pcommon/... -run
TestValueAsString` passes (14/14).

## Documentation

.chloggen entry added.

---------

Signed-off-by: alliasgher <[email protected]>
Signed-off-by: Ali <[email protected]>
2026-04-30 14:14:44 +00:00

521 lines
15 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package pcommon // import "go.opentelemetry.io/collector/pdata/pcommon"
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"go.opentelemetry.io/collector/pdata/internal"
)
// ValueType specifies the type of Value.
type ValueType int32
const (
ValueTypeEmpty ValueType = iota
ValueTypeStr
ValueTypeInt
ValueTypeDouble
ValueTypeBool
ValueTypeMap
ValueTypeSlice
ValueTypeBytes
)
// String returns the string representation of the ValueType.
func (avt ValueType) String() string {
switch avt {
case ValueTypeEmpty:
return "Empty"
case ValueTypeStr:
return "Str"
case ValueTypeBool:
return "Bool"
case ValueTypeInt:
return "Int"
case ValueTypeDouble:
return "Double"
case ValueTypeMap:
return "Map"
case ValueTypeSlice:
return "Slice"
case ValueTypeBytes:
return "Bytes"
}
return ""
}
// Value is a mutable cell containing any value. Typically used as an element of Map or Slice.
// Must use one of NewValue+ functions below to create new instances.
//
// Intended to be passed by value since internally it is just a pointer to actual
// value representation. For the same reason passing by value and calling setters
// will modify the original, e.g.:
//
// func f1(val Value) { val.SetInt(234) }
// func f2() {
// v := NewValueStr("a string")
// f1(v)
// _ := v.Type() // this will return ValueTypeInt
// }
//
// Important: zero-initialized instance is not valid for use. All Value functions below must
// be called only on instances that are created via NewValue+ functions.
type Value internal.ValueWrapper
// NewValueEmpty creates a new Value with an empty value.
func NewValueEmpty() Value {
return newValue(&internal.AnyValue{}, internal.NewState())
}
// NewValueStr creates a new Value with the given string value.
func NewValueStr(v string) Value {
ov := internal.NewAnyValueStringValue()
ov.StringValue = v
orig := internal.NewAnyValue()
orig.Value = ov
return newValue(orig, internal.NewState())
}
// NewValueInt creates a new Value with the given int64 value.
func NewValueInt(v int64) Value {
ov := internal.NewAnyValueIntValue()
ov.IntValue = v
orig := internal.NewAnyValue()
orig.Value = ov
return newValue(orig, internal.NewState())
}
// NewValueDouble creates a new Value with the given float64 value.
func NewValueDouble(v float64) Value {
ov := internal.NewAnyValueDoubleValue()
ov.DoubleValue = v
orig := internal.NewAnyValue()
orig.Value = ov
return newValue(orig, internal.NewState())
}
// NewValueBool creates a new Value with the given bool value.
func NewValueBool(v bool) Value {
ov := internal.NewAnyValueBoolValue()
ov.BoolValue = v
orig := internal.NewAnyValue()
orig.Value = ov
return newValue(orig, internal.NewState())
}
// NewValueMap creates a new Value of map type.
func NewValueMap() Value {
ov := internal.NewAnyValueKvlistValue()
ov.KvlistValue = internal.NewKeyValueList()
orig := internal.NewAnyValue()
orig.Value = ov
return newValue(orig, internal.NewState())
}
// NewValueSlice creates a new Value of array type.
func NewValueSlice() Value {
ov := internal.NewAnyValueArrayValue()
ov.ArrayValue = internal.NewArrayValue()
orig := internal.NewAnyValue()
orig.Value = ov
return newValue(orig, internal.NewState())
}
// NewValueBytes creates a new empty Value of byte type.
func NewValueBytes() Value {
ov := internal.NewAnyValueBytesValue()
orig := internal.NewAnyValue()
orig.Value = ov
return newValue(orig, internal.NewState())
}
func newValue(orig *internal.AnyValue, state *internal.State) Value {
return Value(internal.NewValueWrapper(orig, state))
}
func (v Value) getOrig() *internal.AnyValue {
return internal.GetValueOrig(internal.ValueWrapper(v))
}
func (v Value) getState() *internal.State {
return internal.GetValueState(internal.ValueWrapper(v))
}
// FromRaw sets the value from the given raw value.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) FromRaw(iv any) error {
switch tv := iv.(type) {
case nil:
v.getOrig().Value = nil
case string:
v.SetStr(tv)
case int:
v.SetInt(int64(tv))
case int8:
v.SetInt(int64(tv))
case int16:
v.SetInt(int64(tv))
case int32:
v.SetInt(int64(tv))
case int64:
v.SetInt(tv)
case uint:
v.SetInt(int64(tv))
case uint8:
v.SetInt(int64(tv))
case uint16:
v.SetInt(int64(tv))
case uint32:
v.SetInt(int64(tv))
case uint64:
v.SetInt(int64(tv))
case float32:
v.SetDouble(float64(tv))
case float64:
v.SetDouble(tv)
case bool:
v.SetBool(tv)
case []byte:
v.SetEmptyBytes().FromRaw(tv)
case map[string]any:
return v.SetEmptyMap().FromRaw(tv)
case []any:
return v.SetEmptySlice().FromRaw(tv)
default:
return fmt.Errorf("<Invalid value type %T>", tv)
}
return nil
}
// Type returns the type of the value for this Value.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) Type() ValueType {
switch v.getOrig().Value.(type) {
case *internal.AnyValue_StringValue:
return ValueTypeStr
case *internal.AnyValue_BoolValue:
return ValueTypeBool
case *internal.AnyValue_IntValue:
return ValueTypeInt
case *internal.AnyValue_DoubleValue:
return ValueTypeDouble
case *internal.AnyValue_KvlistValue:
return ValueTypeMap
case *internal.AnyValue_ArrayValue:
return ValueTypeSlice
case *internal.AnyValue_BytesValue:
return ValueTypeBytes
}
return ValueTypeEmpty
}
// Str returns the string value associated with this Value.
// The shorter name is used instead of String to avoid implementing fmt.Stringer interface.
// If the Type() is not ValueTypeStr then returns empty string.
func (v Value) Str() string {
return v.getOrig().GetStringValue()
}
// Int returns the int64 value associated with this Value.
// If the Type() is not ValueTypeInt then returns int64(0).
func (v Value) Int() int64 {
return v.getOrig().GetIntValue()
}
// Double returns the float64 value associated with this Value.
// If the Type() is not ValueTypeDouble then returns float64(0).
func (v Value) Double() float64 {
return v.getOrig().GetDoubleValue()
}
// Bool returns the bool value associated with this Value.
// If the Type() is not ValueTypeBool then returns false.
func (v Value) Bool() bool {
return v.getOrig().GetBoolValue()
}
// Map returns the map value associated with this Value.
// If the function is called on zero-initialized Value or if the Type() is not ValueTypeMap
// then it returns an invalid map. Note that using such map can cause panic.
func (v Value) Map() Map {
kvlist := v.getOrig().GetKvlistValue()
if kvlist == nil {
return Map{}
}
return newMap(&kvlist.Values, internal.GetValueState(internal.ValueWrapper(v)))
}
// Slice returns the slice value associated with this Value.
// If the function is called on zero-initialized Value or if the Type() is not ValueTypeSlice
// then returns an invalid slice. Note that using such slice can cause panic.
func (v Value) Slice() Slice {
arr := v.getOrig().GetArrayValue()
if arr == nil {
return Slice{}
}
return newSlice(&arr.Values, internal.GetValueState(internal.ValueWrapper(v)))
}
// Bytes returns the ByteSlice value associated with this Value.
// If the function is called on zero-initialized Value or if the Type() is not ValueTypeBytes
// then returns an invalid ByteSlice object. Note that using such slice can cause panic.
func (v Value) Bytes() ByteSlice {
bv, ok := v.getOrig().GetValue().(*internal.AnyValue_BytesValue)
if !ok {
return ByteSlice{}
}
return ByteSlice(internal.NewByteSliceWrapper(&bv.BytesValue, internal.GetValueState(internal.ValueWrapper(v))))
}
// SetStr replaces the string value associated with this Value,
// it also changes the type to be ValueTypeStr.
// The shorter name is used instead of SetString to avoid implementing
// fmt.Stringer interface by the corresponding getter method.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) SetStr(sv string) {
v.getState().AssertMutable()
// Delete everything but the AnyValue object itself.
internal.DeleteAnyValue(v.getOrig(), false)
ov := internal.NewAnyValueStringValue()
ov.StringValue = sv
v.getOrig().Value = ov
}
// SetInt replaces the int64 value associated with this Value,
// it also changes the type to be ValueTypeInt.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) SetInt(iv int64) {
v.getState().AssertMutable()
// Delete everything but the AnyValue object itself.
internal.DeleteAnyValue(v.getOrig(), false)
ov := internal.NewAnyValueIntValue()
ov.IntValue = iv
v.getOrig().Value = ov
}
// SetDouble replaces the float64 value associated with this Value,
// it also changes the type to be ValueTypeDouble.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) SetDouble(dv float64) {
v.getState().AssertMutable()
// Delete everything but the AnyValue object itself.
internal.DeleteAnyValue(v.getOrig(), false)
ov := internal.NewAnyValueDoubleValue()
ov.DoubleValue = dv
v.getOrig().Value = ov
}
// SetBool replaces the bool value associated with this Value,
// it also changes the type to be ValueTypeBool.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) SetBool(bv bool) {
v.getState().AssertMutable()
// Delete everything but the AnyValue object itself.
internal.DeleteAnyValue(v.getOrig(), false)
ov := internal.NewAnyValueBoolValue()
ov.BoolValue = bv
v.getOrig().Value = ov
}
// SetEmptyBytes sets value to an empty byte slice and returns it.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) SetEmptyBytes() ByteSlice {
v.getState().AssertMutable()
// Delete everything but the AnyValue object itself.
internal.DeleteAnyValue(v.getOrig(), false)
bv := internal.NewAnyValueBytesValue()
v.getOrig().Value = bv
return ByteSlice(internal.NewByteSliceWrapper(&bv.BytesValue, v.getState()))
}
// SetEmptyMap sets value to an empty map and returns it.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) SetEmptyMap() Map {
v.getState().AssertMutable()
// Delete everything but the AnyValue object itself.
internal.DeleteAnyValue(v.getOrig(), false)
ov := internal.NewAnyValueKvlistValue()
ov.KvlistValue = internal.NewKeyValueList()
v.getOrig().Value = ov
return newMap(&ov.KvlistValue.Values, v.getState())
}
// SetEmptySlice sets value to an empty slice and returns it.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) SetEmptySlice() Slice {
v.getState().AssertMutable()
// Delete everything but the AnyValue object itself.
internal.DeleteAnyValue(v.getOrig(), false)
ov := internal.NewAnyValueArrayValue()
ov.ArrayValue = internal.NewArrayValue()
v.getOrig().Value = ov
return newSlice(&ov.ArrayValue.Values, v.getState())
}
// MoveTo moves the Value from current overriding the destination and
// resetting the current instance to empty value.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) MoveTo(dest Value) {
v.getState().AssertMutable()
dest.getState().AssertMutable()
// If they point to the same data, they are the same, nothing to do.
if v.getOrig() == dest.getOrig() {
return
}
*dest.getOrig() = *v.getOrig()
v.getOrig().Value = nil
}
// CopyTo copies the Value instance overriding the destination.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) CopyTo(dest Value) {
dest.getState().AssertMutable()
internal.CopyAnyValue(dest.getOrig(), v.getOrig())
}
// AsString converts an OTLP Value object of any type to its equivalent string
// representation. This differs from Str which only returns a non-empty value
// if the ValueType is ValueTypeStr.
// Calling this function on zero-initialized Value will cause a panic.
func (v Value) AsString() string {
switch v.Type() {
case ValueTypeEmpty:
return ""
case ValueTypeStr:
return v.Str()
case ValueTypeBool:
return strconv.FormatBool(v.Bool())
case ValueTypeDouble:
return float64AsString(v.Double())
case ValueTypeInt:
return strconv.FormatInt(v.Int(), 10)
case ValueTypeMap:
return marshalJSONNoHTMLEscape(v.Map().AsRaw())
case ValueTypeBytes:
return base64.StdEncoding.EncodeToString(*v.Bytes().getOrig())
case ValueTypeSlice:
return marshalJSONNoHTMLEscape(v.Slice().AsRaw())
default:
return fmt.Sprintf("<Unknown OpenTelemetry attribute value type %q>", v.Type())
}
}
// marshalJSONNoHTMLEscape marshals v as JSON without HTML-escaping "<", ">",
// and "&". This matches the behavior of AsString for ValueTypeStr (which
// returns the raw string) and keeps structured values (maps, slices) free
// of escape sequences like "\u003c" that are only meaningful in HTML contexts.
func marshalJSONNoHTMLEscape(v any) string {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
// Encode cannot fail for the map/slice values produced by AsRaw(), which
// only contain primitive types, so the error is intentionally ignored —
// consistent with the json.Marshal calls it replaces.
_ = enc.Encode(v)
// json.Encoder.Encode always appends a trailing newline; strip it so the
// output matches json.Marshal.
return strings.TrimRight(buf.String(), "\n")
}
// See https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/encoding/json/encode.go;l=585.
// This allows us to avoid using reflection.
func float64AsString(f float64) string {
if math.IsNaN(f) {
return "NaN"
}
if math.IsInf(f, 1) {
return "Infinity"
}
if math.IsInf(f, -1) {
return "-Infinity"
}
// Convert as if by ES6 number to string conversion.
// This matches most other JSON generators.
// See golang.org/issue/6384 and golang.org/issue/14135.
// Like fmt %g, but the exponent cutoffs are different
// and exponents themselves are not padded to two digits.
scratch := [64]byte{}
b := scratch[:0]
abs := math.Abs(f)
fmt := byte('f')
if abs != 0 && (abs < 1e-6 || abs >= 1e21) {
fmt = 'e'
}
b = strconv.AppendFloat(b, f, fmt, -1, 64)
if fmt == 'e' {
// clean up e-09 to e-9
n := len(b)
if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' {
b[n-2] = b[n-1]
b = b[:n-1]
}
}
return string(b)
}
func (v Value) AsRaw() any {
switch v.Type() {
case ValueTypeEmpty:
return nil
case ValueTypeStr:
return v.Str()
case ValueTypeBool:
return v.Bool()
case ValueTypeDouble:
return v.Double()
case ValueTypeInt:
return v.Int()
case ValueTypeBytes:
return v.Bytes().AsRaw()
case ValueTypeMap:
return v.Map().AsRaw()
case ValueTypeSlice:
return v.Slice().AsRaw()
}
return fmt.Sprintf("<Unknown OpenTelemetry value type %q>", v.Type())
}
func (v Value) Equal(c Value) bool {
if v.Type() != c.Type() {
return false
}
switch v.Type() {
case ValueTypeEmpty:
return true
case ValueTypeStr:
return v.Str() == c.Str()
case ValueTypeBool:
return v.Bool() == c.Bool()
case ValueTypeDouble:
return v.Double() == c.Double()
case ValueTypeInt:
return v.Int() == c.Int()
case ValueTypeBytes:
return v.Bytes().Equal(c.Bytes())
case ValueTypeMap:
return v.Map().Equal(c.Map())
case ValueTypeSlice:
return v.Slice().Equal(c.Slice())
}
return false
}