go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/appengine/coordinator/flex/logs/query.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 logs 16 17 import ( 18 "context" 19 20 "google.golang.org/grpc/codes" 21 "google.golang.org/grpc/status" 22 23 logdog "go.chromium.org/luci/logdog/api/endpoints/coordinator/logs/v1" 24 "go.chromium.org/luci/logdog/appengine/coordinator" 25 "go.chromium.org/luci/logdog/common/types" 26 27 "go.chromium.org/luci/common/clock" 28 log "go.chromium.org/luci/common/logging" 29 ds "go.chromium.org/luci/gae/service/datastore" 30 "go.chromium.org/luci/server/auth/realms" 31 ) 32 33 const ( 34 // queryResultLimit is the maximum number of log streams that will be 35 // returned in a single query. If the user requests more, it will be 36 // automatically called at this value. 37 queryResultLimit = 500 38 ) 39 40 // Query returns log stream paths that match the requested query. 41 func (s *server) Query(c context.Context, req *logdog.QueryRequest) (*logdog.QueryResponse, error) { 42 // Non-admin users may not request purged results. 43 canSeePurged := true 44 switch yes, err := coordinator.CheckAdminUser(c); { 45 case err != nil: 46 return nil, status.Error(codes.Internal, "internal server error") 47 case !yes: 48 canSeePurged = false 49 if req.Purged == logdog.QueryRequest_YES { 50 log.Errorf(c, "Non-superuser requested to see purged logs. Denying.") 51 return nil, status.Errorf(codes.InvalidArgument, "non-admin user cannot request purged log streams") 52 } 53 } 54 55 // Scale the maximum number of results based on the number of queries in this 56 // request. If the user specified a maximum result count of zero, use the 57 // default maximum. 58 // 59 // If this scaling results in a limit that is <1 per request, we will return 60 // back a BadRequest error. 61 limit := s.resultLimit 62 if limit == 0 { 63 limit = queryResultLimit 64 } 65 66 // Execute our queries in parallel. 67 resp := logdog.QueryResponse{} 68 e := &queryRunner{ 69 ctx: log.SetField(c, "path", req.Path), 70 req: req, 71 canSeePurged: canSeePurged, 72 limit: limit, 73 } 74 75 startTime := clock.Now(c) 76 if err := e.runQuery(&resp); err != nil { 77 // Transient errors would be handled at the "execute" level, so these are 78 // specific failure errors. We must escalate individual errors to the user. 79 // We will choose the most severe of the resulting errors. 80 log.WithError(err).Errorf(c, "Failed to execute query.") 81 return nil, err 82 } 83 log.Infof(c, "Query took: %s", clock.Now(c).Sub(startTime)) 84 return &resp, nil 85 } 86 87 type queryRunner struct { 88 ctx context.Context 89 req *logdog.QueryRequest 90 canSeePurged bool 91 limit int 92 } 93 94 func (r *queryRunner) runQuery(resp *logdog.QueryResponse) error { 95 if r.limit == 0 { 96 return status.Errorf(codes.InvalidArgument, "query limit is zero") 97 } 98 99 if int(r.req.MaxResults) > 0 && r.limit > int(r.req.MaxResults) { 100 r.limit = int(r.req.MaxResults) 101 } 102 103 q, err := coordinator.NewLogStreamQuery(r.req.Path) 104 if err != nil { 105 log.Fields{ 106 log.ErrorKey: err, 107 "path": r.req.Path, 108 }.Errorf(r.ctx, "Invalid query path.") 109 return status.Errorf(codes.InvalidArgument, "invalid query `path`") 110 } 111 112 pfx := &coordinator.LogPrefix{ID: coordinator.LogPrefixID(q.Prefix)} 113 if err := ds.Get(r.ctx, pfx); err != nil { 114 if err == ds.ErrNoSuchEntity { 115 return coordinator.PermissionDeniedErr(r.ctx) 116 } 117 log.WithError(err).Errorf(r.ctx, "Failed to fetch LogPrefix") 118 return status.Error(codes.Internal, "internal server error") 119 } 120 121 // Old prefixes have no realm set. Fallback to "@legacy". 122 realm := pfx.Realm 123 if realm == "" { 124 realm = realms.Join(r.req.Project, realms.LegacyRealm) 125 } 126 resp.Project, resp.Realm = realms.Split(realm) 127 128 // Check the caller is allowed to enumerate streams under this prefix. 129 if err := coordinator.CheckPermission(r.ctx, coordinator.PermLogsList, q.Prefix, realm); err != nil { 130 return err 131 } 132 133 // The stored realm project **must** match the requested project. This error 134 // should never happen. If it does, it indicates some kind of a corruption. 135 if resp.Project != r.req.Project { 136 log.Errorf(r.ctx, "Expected a realm in project %q, but saw %q", r.req.Project, realm) 137 return status.Error(codes.Internal, "internal server error") 138 } 139 140 if err := q.SetCursor(r.ctx, r.req.Next); err != nil { 141 log.Fields{ 142 log.ErrorKey: err, 143 "cursor": r.req.Next, 144 }.Errorf(r.ctx, "Failed to SetCursor.") 145 return status.Errorf(codes.InvalidArgument, "invalid `next` value") 146 } 147 148 q.OnlyContentType(r.req.ContentType) 149 if st := r.req.StreamType; st != nil { 150 if err := q.OnlyStreamType(st.Value); err != nil { 151 return status.Errorf(codes.InvalidArgument, "invalid query `streamType`: %s", st.Value) 152 } 153 } 154 155 // By default q wll exclude purged data. 156 // 157 // If the user is allowed to, and `r.Purged in (YES, BOTH)`, include purged 158 // results in the result. 159 if r.canSeePurged && r.req.Purged != logdog.QueryRequest_NO { 160 q.IncludePurged() 161 // If the user requested to ONLY see purged results, further restrict the 162 // query. 163 if r.req.Purged == logdog.QueryRequest_YES { 164 q.OnlyPurged() 165 } 166 } 167 168 // Add tag constraints. 169 for k, v := range r.req.Tags { 170 if err := types.ValidateTag(k, v); err != nil { 171 log.Fields{ 172 log.ErrorKey: err, 173 "key": k, 174 "value": v, 175 }.Errorf(r.ctx, "Invalid tag constraint.") 176 return status.Errorf(codes.InvalidArgument, "invalid tag constraint: %q", k) 177 } 178 } 179 q.MustHaveTags(r.req.Tags) 180 181 // The "State" boolean in the query request populates two pieces of data: 182 // 1) The Desc field in logdog.QueryResponse_Stream 183 // 2) The State field (of type logdog.LogStreamState) in 184 // logdog.QueryResponse_Stream. 185 // 186 // Note that the logdog.LogStreamState is actually composed of data from both 187 // * a coordinator.LogStream 188 // * a coordinator.LogStreamState 189 // 190 // As far as I can tell, there's no reason for this complexity, it's just 191 // confusing. 192 // 193 // To handle this, we have two arrays populated during the Run query: 194 // * logStreamStates holds the coordinator LogStreamState objects we need 195 // to pull from datastore. 196 // * respLogStreamStates holds the similarly-named response objects. 197 // 198 // We partially populate the content of the respLogStreamStates objects during 199 // the execution of Run (the portion populated from the coordinator.LogStream 200 // object). 201 // 202 // After the query, if these arrays are non-empty, we load the LogStreamState 203 // objects from datastore, and populate the rest of the response objects. 204 var logStreamStates []*coordinator.LogStreamState 205 var respLogStreamStates []*logdog.LogStreamState 206 207 err = q.Run(r.ctx, func(ls *coordinator.LogStream, cb ds.CursorCB) error { 208 toAdd := &logdog.QueryResponse_Stream{ 209 Path: string(ls.Path()), 210 } 211 resp.Streams = append(resp.Streams, toAdd) 212 if r.req.State { 213 // ls.State returns a coordinator.LogStreamState object with just its 214 // Parent key field populated. 215 logStreamStates = append(logStreamStates, ls.State(r.ctx)) 216 217 // generate and fill the response LogStreamState object, then track it for 218 // later. 219 toAdd.State = &logdog.LogStreamState{} 220 fillStateFromLogStream(toAdd.State, ls) 221 respLogStreamStates = append(respLogStreamStates, toAdd.State) 222 223 if desc, err := ls.DescriptorProto(); err != nil { 224 log.Errorf(r.ctx, "processing %q: loading descriptor: %s", toAdd.Path, err) 225 } else { 226 toAdd.Desc = desc 227 } 228 } 229 if len(resp.Streams) == r.limit { 230 cursor, err := cb() 231 if err != nil { 232 return err 233 } 234 resp.Next = cursor.String() 235 return ds.Stop 236 } 237 return nil 238 }) 239 if err != nil { 240 log.Fields{ 241 log.ErrorKey: err, 242 }.Errorf(r.ctx, "Failed to execute query.") 243 return status.Errorf(codes.Internal, "failed to execute query: %s", err) 244 } 245 246 if len(logStreamStates) > 0 { 247 if err := ds.Get(r.ctx, logStreamStates); err != nil { 248 log.WithError(err).Errorf(r.ctx, "Failed to load log stream states.") 249 return status.Errorf(codes.Internal, "failed to load log stream states: %s", err) 250 } 251 for i, state := range logStreamStates { 252 fillStateFromLogStreamState(respLogStreamStates[i], state) 253 } 254 } 255 256 return nil 257 }