go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/output/memory/memory.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package memory implements an in-memory sink for the logdog Butler. 16 // 17 // This is meant for absorbing log data during testing of applications 18 // which expect a live Butler. 19 package memory 20 21 import ( 22 "strings" 23 "sync" 24 25 "google.golang.org/protobuf/proto" 26 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/logdog/api/logpb" 29 "go.chromium.org/luci/logdog/client/butler/bootstrap" 30 "go.chromium.org/luci/logdog/client/butler/output" 31 ) 32 33 // Output implements the butler output.Output interface, but just 34 // accumulates the data in memory. 35 // 36 // For simplicity, this only retains a subset of the data transmitted 37 // by SendBundle (e.g. no timestamps, indexes, etc.). 38 // 39 // This assumes that SendBundle is called in order. 40 type Output struct { 41 mu sync.RWMutex 42 streams map[streamKey]*FakeStream 43 stats output.StatsBase 44 } 45 46 // GetStream returns the *FakeStream corresponding to the given prefix and 47 // stream name. 48 // 49 // If no such stream was opened, returns nil. 50 func (o *Output) GetStream(prefix, stream string) *FakeStream { 51 o.mu.Lock() 52 defer o.mu.Unlock() 53 return o.streams[streamKey{prefix, stream}] 54 } 55 56 var _ output.Output = (*Output)(nil) 57 58 type streamKey struct { 59 Prefix string 60 Name string 61 } 62 63 // FakeStream holds the data recorded by a single stream in Output. 64 type FakeStream struct { 65 stype logpb.StreamType 66 tags map[string]string 67 68 mu sync.Mutex 69 lastIsFinal bool 70 data []*strings.Builder 71 } 72 73 // StreamType returns the categorization of this stream 74 // (TEXT, BINARY, DATAGRAM). 75 func (fs *FakeStream) StreamType() logpb.StreamType { 76 return fs.stype 77 } 78 79 // Tags returns any tags set for this stream. 80 func (fs *FakeStream) Tags() map[string]string { 81 ret := make(map[string]string, len(fs.tags)) 82 for k, v := range fs.tags { 83 ret[k] = v 84 } 85 return ret 86 } 87 88 // AllData returns all datagrams for this stream. 89 // 90 // If this is a BINARY or TEXT stream, this will always 91 // return exactly one string. 92 // 93 // If fs == nil, returns nil. 94 func (fs *FakeStream) AllData() []string { 95 if fs == nil { 96 return nil 97 } 98 99 fs.mu.Lock() 100 defer fs.mu.Unlock() 101 ret := make([]string, len(fs.data)) 102 for i, dat := range fs.data { 103 ret[i] = dat.String() 104 } 105 return ret 106 } 107 108 // LastData returns the last datagram for this stream. 109 // 110 // If this is a BINARY or TEXT stream, this returns the 111 // stream data. 112 // 113 // If fs == nil, returns "". 114 func (fs *FakeStream) LastData() string { 115 if fs == nil { 116 return "" 117 } 118 119 fs.mu.Lock() 120 defer fs.mu.Unlock() 121 122 return fs.data[len(fs.data)-1].String() 123 } 124 125 func (fs *FakeStream) addData(be *logpb.ButlerLogBundle_Entry) { 126 fs.mu.Lock() 127 defer fs.mu.Unlock() 128 129 switch fs.stype { 130 case logpb.StreamType_TEXT: 131 for _, logEntry := range be.Logs { 132 for _, line := range logEntry.GetText().Lines { 133 fs.data[0].Write(line.Value) 134 fs.data[0].WriteString(line.Delimiter) 135 } 136 } 137 case logpb.StreamType_BINARY: 138 for _, logEntry := range be.Logs { 139 fs.data[0].Write(logEntry.GetBinary().Data) 140 } 141 case logpb.StreamType_DATAGRAM: 142 for _, logEntry := range be.Logs { 143 dg := logEntry.GetDatagram() 144 if fs.lastIsFinal { 145 fs.data = append(fs.data, &strings.Builder{}) 146 fs.lastIsFinal = false 147 } 148 fs.data[len(fs.data)-1].Write(dg.Data) 149 if dg.Partial == nil { 150 fs.lastIsFinal = true 151 } 152 } 153 default: 154 panic(errors.Reason("unknown StreamType: %s", fs.stype).Err()) 155 } 156 } 157 158 // SendBundle implements output.Output 159 func (o *Output) SendBundle(b *logpb.ButlerLogBundle) error { 160 o.mu.Lock() 161 defer o.mu.Unlock() 162 o.stats.F.SentMessages += int64(len(b.Entries)) 163 o.stats.F.SentBytes += int64(proto.Size(b)) 164 165 if o.streams == nil { 166 o.streams = map[streamKey]*FakeStream{} 167 } 168 169 for _, bundleEntry := range b.Entries { 170 sk := streamKey{bundleEntry.Desc.Prefix, bundleEntry.Desc.Name} 171 cur, ok := o.streams[sk] 172 if !ok { 173 cur = &FakeStream{ 174 stype: bundleEntry.Desc.StreamType, 175 data: []*strings.Builder{{}}, 176 } 177 cur.tags = make(map[string]string, len(bundleEntry.Desc.Tags)) 178 for k, v := range bundleEntry.Desc.Tags { 179 cur.tags[k] = v 180 } 181 o.streams[sk] = cur 182 } 183 184 cur.addData(bundleEntry) 185 } 186 187 return nil 188 } 189 190 // MaxSendBundles implements output.Output 191 func (o *Output) MaxSendBundles() int { 192 return 1 193 } 194 195 // Stats implements output.Output 196 func (o *Output) Stats() output.Stats { 197 o.mu.RLock() 198 defer o.mu.RUnlock() 199 statsCp := o.stats 200 return &statsCp 201 } 202 203 // URLConstructionEnv implements output.Output 204 func (o *Output) URLConstructionEnv() bootstrap.Environment { 205 return bootstrap.Environment{ 206 Project: "memory", 207 Prefix: "memory", 208 } 209 } 210 211 // MaxSize returns a large number instead of 0 because butler has bugs. 212 func (o *Output) MaxSize() int { return 1024 * 1024 * 1024 } 213 214 // Close implements output.Output 215 func (o *Output) Close() {}