// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package attribute // import "go.opentelemetry.io/otel/attribute" import ( "bytes" "sync" "sync/atomic" ) type ( // Encoder is a mechanism for serializing an attribute set into a specific // string representation that supports caching, to avoid repeated // serialization. An example could be an exporter encoding the attribute // set into a wire representation. Encoder interface { // Encode returns the serialized encoding of the attribute set using // its Iterator. This result may be cached by a attribute.Set. Encode(iterator Iterator) string // ID returns a value that is unique for each class of attribute // encoder. Attribute encoders allocate these using `NewEncoderID`. ID() EncoderID } // EncoderID is used to identify distinct Encoder // implementations, for caching encoded results. EncoderID struct { value uint64 } // defaultAttrEncoder uses a sync.Pool of buffers to reduce the number of // allocations used in encoding attributes. This implementation encodes a // comma-separated list of key=value, with '/'-escaping of '=', ',', and // '\'. defaultAttrEncoder struct { // pool is a pool of attribute set builders. The buffers in this pool // grow to a size that most attribute encodings will not allocate new // memory. pool sync.Pool // *bytes.Buffer } ) // escapeChar is used to ensure uniqueness of the attribute encoding where // keys or values contain either '=' or ','. Since there is no parser needed // for this encoding and its only requirement is to be unique, this choice is // arbitrary. Users will see these in some exporters (e.g., stdout), so the // backslash ('\') is used as a conventional choice. const escapeChar = '\\' var ( _ Encoder = &defaultAttrEncoder{} // encoderIDCounter is for generating IDs for other attribute encoders. encoderIDCounter uint64 defaultEncoderOnce sync.Once defaultEncoderID = NewEncoderID() defaultEncoderInstance *defaultAttrEncoder ) // NewEncoderID returns a unique attribute encoder ID. It should be called // once per each type of attribute encoder. Preferably in init() or in var // definition. func NewEncoderID() EncoderID { return EncoderID{value: atomic.AddUint64(&encoderIDCounter, 1)} } // DefaultEncoder returns an attribute encoder that encodes attributes in such // a way that each escaped attribute's key is followed by an equal sign and // then by an escaped attribute's value. All key-value pairs are separated by // a comma. // // Escaping is done by prepending a backslash before either a backslash, equal // sign or a comma. func DefaultEncoder() Encoder { defaultEncoderOnce.Do(func() { defaultEncoderInstance = &defaultAttrEncoder{ pool: sync.Pool{ New: func() interface{} { return &bytes.Buffer{} }, }, } }) return defaultEncoderInstance } // Encode is a part of an implementation of the AttributeEncoder interface. func (d *defaultAttrEncoder) Encode(iter Iterator) string { buf := d.pool.Get().(*bytes.Buffer) defer d.pool.Put(buf) buf.Reset() for iter.Next() { i, keyValue := iter.IndexedAttribute() if i > 0 { _, _ = buf.WriteRune(',') } copyAndEscape(buf, string(keyValue.Key)) _, _ = buf.WriteRune('=') if keyValue.Value.Type() == STRING { copyAndEscape(buf, keyValue.Value.AsString()) } else { _, _ = buf.WriteString(keyValue.Value.Emit()) } } return buf.String() } // ID is a part of an implementation of the AttributeEncoder interface. func (*defaultAttrEncoder) ID() EncoderID { return defaultEncoderID } // copyAndEscape escapes `=`, `,` and its own escape character (`\`), // making the default encoding unique. func copyAndEscape(buf *bytes.Buffer, val string) { for _, ch := range val { switch ch { case '=', ',', escapeChar: _, _ = buf.WriteRune(escapeChar) } _, _ = buf.WriteRune(ch) } } // Valid returns true if this encoder ID was allocated by // `NewEncoderID`. Invalid encoder IDs will not be cached. func (id EncoderID) Valid() bool { return id.value != 0 }