go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/coordinator/stream.go (about) 1 // Copyright 2015 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 coordinator 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "time" 22 23 logdog "go.chromium.org/luci/logdog/api/endpoints/coordinator/logs/v1" 24 "go.chromium.org/luci/logdog/api/logpb" 25 "go.chromium.org/luci/logdog/common/types" 26 ) 27 28 // StreamState represents the client-side state of the log stream. 29 // 30 // It is a type-promoted version of logdog.LogStreamState. 31 type StreamState struct { 32 // Created is the time, represented as a UTC RFC3339 string, when the log 33 // stream was created. 34 Created time.Time 35 // Updated is the time, represented as a UTC RFC3339 string, when the log 36 // stream was last updated. 37 Updated time.Time 38 39 // TerminalIndex is the stream index of the log stream's terminal message. If 40 // its value is <0, then the log stream has not terminated yet. 41 // In this case, FinishedIndex is the index of that terminal message. 42 TerminalIndex types.MessageIndex 43 44 // Archived is true if the stream is marked as archived. 45 Archived bool 46 // ArchiveIndexURL is the Google Storage URL where the log stream's index is 47 // archived. 48 ArchiveIndexURL string 49 // ArchiveStreamURL is the Google Storage URL where the log stream's raw 50 // stream data is archived. If this is not empty, the log stream is considered 51 // archived. 52 ArchiveStreamURL string 53 // ArchiveDataURL is the Google Storage URL where the log stream's assembled 54 // data is archived. If this is not empty, the log stream is considered 55 // archived. 56 ArchiveDataURL string 57 58 // Purged indicates the purged state of a log. A log that has been purged is 59 // only acknowledged to administrative clients. 60 Purged bool 61 } 62 63 // LogStream is returned metadata about a log stream. 64 type LogStream struct { 65 // Project is the log stream's project. 66 Project string 67 // Path is the path of the log stream. 68 Path types.StreamPath 69 70 // Desc is the log stream's descriptor. 71 // 72 // TODO(iannucci): Do not embed proto messages! This should be a pointer, and 73 // all existing shallow clones of it should be converted to proto.Clone or 74 // similar. 75 Desc logpb.LogStreamDescriptor 76 77 // State is the stream's current state. 78 State StreamState 79 } 80 81 func loadLogStream(proj string, path types.StreamPath, s *logdog.LogStreamState, d *logpb.LogStreamDescriptor) *LogStream { 82 ls := LogStream{ 83 Project: proj, 84 Path: path, 85 } 86 if d != nil { 87 ls.Desc = *d 88 } 89 if s != nil { 90 ls.State = StreamState{ 91 Created: s.Created.AsTime(), 92 TerminalIndex: types.MessageIndex(s.TerminalIndex), 93 Purged: s.Purged, 94 } 95 96 if a := s.Archive; a != nil { 97 ls.State.Archived = true 98 ls.State.ArchiveIndexURL = a.IndexUrl 99 ls.State.ArchiveStreamURL = a.StreamUrl 100 ls.State.ArchiveDataURL = a.DataUrl 101 } 102 } 103 return &ls 104 } 105 106 // Stream is an interface to Coordinator stream-level commands. It is bound to 107 // and operates on a single log stream path. 108 type Stream struct { 109 // c is the Coordinator instance that this Stream is bound to. 110 c *Client 111 112 // project is this stream's project. 113 project string 114 // path is the log stream's prefix. 115 path types.StreamPath 116 } 117 118 // State fetches the LogStreamDescriptor for a given log stream. 119 func (s *Stream) State(ctx context.Context) (*LogStream, error) { 120 req := logdog.GetRequest{ 121 Project: string(s.project), 122 Path: string(s.path), 123 State: true, 124 LogCount: -1, // Don't fetch any logs. 125 } 126 127 resp, err := s.c.C.Get(ctx, &req) 128 if err != nil { 129 return nil, normalizeError(err) 130 } 131 132 path := types.StreamPath(req.Path) 133 if desc := resp.Desc; desc != nil { 134 path = desc.Path() 135 } 136 137 return loadLogStream(resp.Project, path, resp.State, resp.Desc), nil 138 } 139 140 // Get retrieves log stream entries from the Coordinator. The supplied 141 // parameters shape which entries are requested and what information is 142 // returned. 143 func (s *Stream) Get(ctx context.Context, params ...GetParam) ([]*logpb.LogEntry, error) { 144 p := getParamsInst{ 145 r: logdog.GetRequest{ 146 Project: string(s.project), 147 Path: string(s.path), 148 }, 149 } 150 for _, param := range params { 151 param.applyGet(&p) 152 } 153 154 if p.stateP != nil { 155 p.r.State = true 156 } 157 158 resp, err := s.c.C.Get(ctx, &p.r) 159 if err != nil { 160 return nil, normalizeError(err) 161 } 162 if err := loadStatePointer(p.stateP, resp); err != nil { 163 return nil, err 164 } 165 return resp.Logs, nil 166 } 167 168 // Tail performs a tail call, returning the last log entry in the stream. If 169 // stateP is not nil, the stream's state will be requested and loaded into the 170 // variable. 171 func (s *Stream) Tail(ctx context.Context, params ...TailParam) (*logpb.LogEntry, error) { 172 p := tailParamsInst{ 173 r: logdog.TailRequest{ 174 Project: string(s.project), 175 Path: string(s.path), 176 }, 177 } 178 for _, param := range params { 179 param.applyTail(&p) 180 } 181 182 resp, err := s.c.C.Tail(ctx, &p.r) 183 if err != nil { 184 return nil, normalizeError(err) 185 } 186 if err := loadStatePointer(p.stateP, resp); err != nil { 187 return nil, err 188 } 189 190 switch len(resp.Logs) { 191 case 0: 192 return nil, nil 193 194 case 1: 195 le := resp.Logs[0] 196 if p.complete { 197 if dg := le.GetDatagram(); dg != nil && dg.Partial != nil { 198 // This is a partial; datagram. Fetch and assemble the full datagram. 199 return s.fetchFullDatagram(ctx, le, true) 200 } 201 } 202 return le, nil 203 204 default: 205 return nil, fmt.Errorf("tail call returned %d logs", len(resp.Logs)) 206 } 207 } 208 209 func (s *Stream) fetchFullDatagram(ctx context.Context, le *logpb.LogEntry, fetchIfMid bool) (*logpb.LogEntry, error) { 210 // Re-evaluate our partial state. 211 dg := le.GetDatagram() 212 if dg == nil { 213 return nil, fmt.Errorf("entry is not a datagram") 214 } 215 216 p := dg.Partial 217 if p == nil { 218 // Not partial, return the full message. 219 return le, nil 220 } 221 222 if uint64(p.Index) > le.StreamIndex { 223 // Something is wrong. The datagram identifies itself as an index in the 224 // stream that exceeds the actual number of entries in the stream. 225 return nil, fmt.Errorf("malformed partial datagram; index (%d) > stream index (%d)", 226 p.Index, le.StreamIndex) 227 } 228 229 if !p.Last { 230 // This is the last log entry (b/c we Tail'd), but it is part of a larger 231 // datagram. We can't fetch the full datagram since presumably the remainder 232 // doesn't exist. Therefore, fetch the previous datagram. 233 switch { 234 case !fetchIfMid: 235 return nil, fmt.Errorf("mid-fragment partial datagram not allowed") 236 237 case uint64(p.Index) == le.StreamIndex: 238 // If we equal the stream index, then we are the first datagram in the 239 // stream, so return nil. 240 return nil, nil 241 242 default: 243 // Perform a Get on the previous entry in the stream. 244 prevIdx := le.StreamIndex - uint64(p.Index) - 1 245 logs, err := s.Get(ctx, Index(types.MessageIndex(prevIdx)), LimitCount(1)) 246 if err != nil { 247 return nil, fmt.Errorf("failed to get previous datagram (%d): %s", prevIdx, err) 248 } 249 250 if len(logs) != 1 || logs[0].StreamIndex != prevIdx { 251 return nil, fmt.Errorf("previous datagram (%d) not returned", prevIdx) 252 } 253 if le, err = s.fetchFullDatagram(ctx, logs[0], false); err != nil { 254 return nil, fmt.Errorf("failed to recurse to previous datagram (%d): %s", prevIdx, err) 255 } 256 return le, nil 257 } 258 } 259 260 // If this is "Last", but it's also index 0, then it is a partial datagram 261 // with one entry. Weird ... but whatever. 262 if p.Index == 0 { 263 dg.Partial = nil 264 return le, nil 265 } 266 267 // Get the intermediate logs. 268 startIdx := types.MessageIndex(le.StreamIndex - uint64(p.Index)) 269 count := int(p.Index) 270 logs, err := s.Get(ctx, Index(startIdx), LimitCount(count)) 271 if err != nil { 272 return nil, fmt.Errorf("failed to get intermediate logs [%d .. %d]: %s", 273 startIdx, startIdx+types.MessageIndex(count)-1, err) 274 } 275 276 if len(logs) < count { 277 return nil, fmt.Errorf("incomplete intermediate logs results (%d < %d)", len(logs), count) 278 } 279 logs = append(logs[:count], le) 280 281 // Construct the full datagram. 282 aggregate := make([]byte, 0, int(p.Size)) 283 for i, ple := range logs { 284 chunkDg := ple.GetDatagram() 285 if chunkDg == nil { 286 return nil, fmt.Errorf("intermediate datagram #%d is not a datagram", i) 287 } 288 chunkP := chunkDg.Partial 289 if chunkP == nil { 290 return nil, fmt.Errorf("intermediate datagram #%d is not partial", i) 291 } 292 if int(chunkP.Index) != i { 293 return nil, fmt.Errorf("intermediate datagram #%d does not have a contiguous index (%d)", i, chunkP.Index) 294 } 295 if chunkP.Size != p.Size { 296 return nil, fmt.Errorf("inconsistent datagram size (%d != %d)", chunkP.Size, p.Size) 297 } 298 if uint64(len(aggregate))+uint64(len(chunkDg.Data)) > p.Size { 299 return nil, fmt.Errorf("appending chunk data would exceed the declared size (%d > %d)", 300 len(aggregate)+len(chunkDg.Data), p.Size) 301 } 302 aggregate = append(aggregate, chunkDg.Data...) 303 } 304 305 if uint64(len(aggregate)) != p.Size { 306 return nil, fmt.Errorf("reassembled datagram length (%d) differs from declared length (%d)", len(aggregate), p.Size) 307 } 308 309 le = logs[0] 310 dg = le.GetDatagram() 311 dg.Data = aggregate 312 dg.Partial = nil 313 return le, nil 314 } 315 316 func loadStatePointer(stateP *LogStream, resp *logdog.GetResponse) error { 317 if stateP == nil { 318 return nil 319 } 320 321 // The service should always return this when requested, but handle the case 322 // where it doesn't for completeness. 323 if resp.Desc == nil { 324 return errors.New("descriptor was not returned") 325 } 326 327 ls := loadLogStream(resp.Project, resp.Desc.Path(), resp.State, resp.Desc) 328 *stateP = *ls 329 return nil 330 }