1 // Copyright The OpenTelemetry Authors
2 // SPDX-License-Identifier: Apache-2.0
3 4 package attribute // import "go.opentelemetry.io/otel/attribute"
5 6 import (
7 "bytes"
8 "sync"
9 "sync/atomic"
10 )
11 12 type (
13 // Encoder is a mechanism for serializing an attribute set into a specific
14 // string representation that supports caching, to avoid repeated
15 // serialization. An example could be an exporter encoding the attribute
16 // set into a wire representation.
17 Encoder interface {
18 // Encode returns the serialized encoding of the attribute set using
19 // its Iterator. This result may be cached by a attribute.Set.
20 Encode(iterator Iterator) string
21 22 // ID returns a value that is unique for each class of attribute
23 // encoder. Attribute encoders allocate these using `NewEncoderID`.
24 ID() EncoderID
25 }
26 27 // EncoderID is used to identify distinct Encoder
28 // implementations, for caching encoded results.
29 EncoderID struct {
30 value uint64
31 }
32 33 // defaultAttrEncoder uses a sync.Pool of buffers to reduce the number of
34 // allocations used in encoding attributes. This implementation encodes a
35 // comma-separated list of key=value, with '/'-escaping of '=', ',', and
36 // '\'.
37 defaultAttrEncoder struct {
38 // pool is a pool of attribute set builders. The buffers in this pool
39 // grow to a size that most attribute encodings will not allocate new
40 // memory.
41 pool sync.Pool // *bytes.Buffer
42 }
43 )
44 45 // escapeChar is used to ensure uniqueness of the attribute encoding where
46 // keys or values contain either '=' or ','. Since there is no parser needed
47 // for this encoding and its only requirement is to be unique, this choice is
48 // arbitrary. Users will see these in some exporters (e.g., stdout), so the
49 // backslash ('\') is used as a conventional choice.
50 const escapeChar = '\\'
51 52 var (
53 _ Encoder = &defaultAttrEncoder{}
54 55 // encoderIDCounter is for generating IDs for other attribute encoders.
56 encoderIDCounter uint64
57 58 defaultEncoderOnce sync.Once
59 defaultEncoderID = NewEncoderID()
60 defaultEncoderInstance *defaultAttrEncoder
61 )
62 63 // NewEncoderID returns a unique attribute encoder ID. It should be called
64 // once per each type of attribute encoder. Preferably in init() or in var
65 // definition.
66 func NewEncoderID() EncoderID {
67 return EncoderID{value: atomic.AddUint64(&encoderIDCounter, 1)}
68 }
69 70 // DefaultEncoder returns an attribute encoder that encodes attributes in such
71 // a way that each escaped attribute's key is followed by an equal sign and
72 // then by an escaped attribute's value. All key-value pairs are separated by
73 // a comma.
74 //
75 // Escaping is done by prepending a backslash before either a backslash, equal
76 // sign or a comma.
77 func DefaultEncoder() Encoder {
78 defaultEncoderOnce.Do(func() {
79 defaultEncoderInstance = &defaultAttrEncoder{
80 pool: sync.Pool{
81 New: func() any {
82 return &bytes.Buffer{}
83 },
84 },
85 }
86 })
87 return defaultEncoderInstance
88 }
89 90 // Encode is a part of an implementation of the AttributeEncoder interface.
91 func (d *defaultAttrEncoder) Encode(iter Iterator) string {
92 buf := d.pool.Get().(*bytes.Buffer)
93 defer d.pool.Put(buf)
94 buf.Reset()
95 96 for iter.Next() {
97 i, keyValue := iter.IndexedAttribute()
98 if i > 0 {
99 _ = buf.WriteByte(',')
100 }
101 copyAndEscape(buf, string(keyValue.Key))
102 103 _ = buf.WriteByte('=')
104 105 if keyValue.Value.Type() == STRING {
106 copyAndEscape(buf, keyValue.Value.AsString())
107 } else {
108 _, _ = buf.WriteString(keyValue.Value.Emit())
109 }
110 }
111 return buf.String()
112 }
113 114 // ID is a part of an implementation of the AttributeEncoder interface.
115 func (*defaultAttrEncoder) ID() EncoderID {
116 return defaultEncoderID
117 }
118 119 // copyAndEscape escapes `=`, `,` and its own escape character (`\`),
120 // making the default encoding unique.
121 func copyAndEscape(buf *bytes.Buffer, val string) {
122 for _, ch := range val {
123 switch ch {
124 case '=', ',', escapeChar:
125 _ = buf.WriteByte(escapeChar)
126 }
127 _, _ = buf.WriteRune(ch)
128 }
129 }
130 131 // Valid reports whether this encoder ID was allocated by
132 // [NewEncoderID]. Invalid encoder IDs will not be cached.
133 func (id EncoderID) Valid() bool {
134 return id.value != 0
135 }
136