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  }