go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/accesslog/interceptor.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 accesslog implements an gRPC interceptor that logs calls to a
    16  // BigQuery table.
    17  package accesslog
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"go.opentelemetry.io/otel/trace"
    25  	codepb "google.golang.org/genproto/googleapis/rpc/code"
    26  	"google.golang.org/grpc"
    27  	"google.golang.org/grpc/metadata"
    28  	"google.golang.org/grpc/status"
    29  
    30  	"go.chromium.org/luci/common/clock"
    31  	"go.chromium.org/luci/server"
    32  	"go.chromium.org/luci/server/auth"
    33  	"go.chromium.org/luci/server/auth/authdb"
    34  	"go.chromium.org/luci/server/bqlog"
    35  
    36  	cipdpb "go.chromium.org/luci/cipd/api/cipd/v1"
    37  	"go.chromium.org/luci/cipd/common"
    38  )
    39  
    40  func init() {
    41  	bqlog.RegisterSink(bqlog.Sink{
    42  		Prototype: &cipdpb.AccessLogEntry{},
    43  		Table:     "access",
    44  	})
    45  }
    46  
    47  // NewUnaryServerInterceptor returns an interceptor that logs requests.
    48  func NewUnaryServerInterceptor(opts *server.Options) grpc.UnaryServerInterceptor {
    49  	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
    50  		if !strings.HasPrefix(info.FullMethod, "/cipd.") {
    51  			return handler(ctx, req)
    52  		}
    53  
    54  		start := clock.Now(ctx)
    55  		state := auth.GetState(ctx)
    56  
    57  		entry := &cipdpb.AccessLogEntry{
    58  			Method:         info.FullMethod,
    59  			Timestamp:      start.UnixNano() / 1000,
    60  			CallIdentity:   string(state.User().Identity),
    61  			PeerIdentity:   string(state.PeerIdentity()),
    62  			PeerIp:         state.PeerIP().String(),
    63  			ServiceVersion: opts.ContainerImageID,
    64  			ProcessId:      opts.Hostname,
    65  			RequestId:      trace.SpanContextFromContext(ctx).TraceID().String(),
    66  			AuthDbRev:      authdb.Revision(state.DB()),
    67  		}
    68  		if md, _ := metadata.FromIncomingContext(ctx); len(md["user-agent"]) > 0 {
    69  			entry.UserAgent = strings.Join(md["user-agent"], " ")
    70  		}
    71  
    72  		// Extract interesting information from recognized request body types.
    73  		extractFieldsFromRequest(entry, req)
    74  
    75  		panicking := true
    76  		defer func() {
    77  			entry.ResponseTimeUsec = clock.Since(ctx, start).Microseconds()
    78  			switch {
    79  			case panicking:
    80  				entry.ResponseCode = "INTERNAL"
    81  				entry.ResponseErr = "panic"
    82  			case err == nil:
    83  				entry.ResponseCode = "OK"
    84  			default:
    85  				entry.ResponseCode = codepb.Code_name[int32(status.Code(err))]
    86  				entry.ResponseErr = err.Error()
    87  			}
    88  			bqlog.Log(ctx, entry)
    89  		}()
    90  
    91  		resp, err = handler(ctx, req)
    92  		panicking = false
    93  		return
    94  	}
    95  }
    96  
    97  func extractFieldsFromRequest(entry *cipdpb.AccessLogEntry, req any) {
    98  	if x, ok := req.(interface{ GetPackage() string }); ok {
    99  		entry.Package = x.GetPackage()
   100  	} else if x, ok := req.(interface{ GetPrefix() string }); ok {
   101  		entry.Package = x.GetPrefix()
   102  	}
   103  
   104  	if x, ok := req.(interface{ GetInstance() *cipdpb.ObjectRef }); ok {
   105  		entry.Instance = instanceID(x.GetInstance())
   106  	} else if x, ok := req.(interface{ GetObject() *cipdpb.ObjectRef }); ok {
   107  		entry.Instance = instanceID(x.GetObject())
   108  	}
   109  
   110  	if x, ok := req.(interface{ GetVersion() string }); ok {
   111  		entry.Version = x.GetVersion()
   112  	}
   113  
   114  	if x, ok := req.(interface{ GetTags() []*cipdpb.Tag }); ok {
   115  		entry.Tags = tagList(x.GetTags())
   116  	}
   117  
   118  	if x, ok := req.(interface {
   119  		GetMetadata() []*cipdpb.InstanceMetadata
   120  	}); ok {
   121  		entry.Metadata = metadataKeys(x.GetMetadata())
   122  	}
   123  
   124  	// Few one offs that do not follow the pattern.
   125  	switch r := req.(type) {
   126  	case *cipdpb.Ref:
   127  		entry.Version = r.Name
   128  	case *cipdpb.DeleteRefRequest:
   129  		entry.Version = r.Name
   130  	case *cipdpb.ListMetadataRequest:
   131  		entry.Metadata = r.Keys
   132  	case *cipdpb.DescribeInstanceRequest:
   133  		if r.DescribeRefs {
   134  			entry.Flags = append(entry.Flags, "refs")
   135  		}
   136  		if r.DescribeTags {
   137  			entry.Flags = append(entry.Flags, "tags")
   138  		}
   139  		if r.DescribeProcessors {
   140  			entry.Flags = append(entry.Flags, "processors")
   141  		}
   142  		if r.DescribeMetadata {
   143  			entry.Flags = append(entry.Flags, "metadata")
   144  		}
   145  	}
   146  }
   147  
   148  func instanceID(ref *cipdpb.ObjectRef) string {
   149  	if ref == nil {
   150  		return ""
   151  	}
   152  	if err := common.ValidateObjectRef(ref, common.AnyHash); err != nil {
   153  		return fmt.Sprintf("INVALID:%s", err)
   154  	}
   155  	return common.ObjectRefToInstanceID(ref)
   156  }
   157  
   158  func tagList(tags []*cipdpb.Tag) []string {
   159  	out := make([]string, len(tags))
   160  	for i, t := range tags {
   161  		out[i] = common.JoinInstanceTag(t)
   162  	}
   163  	return out
   164  }
   165  
   166  func metadataKeys(md []*cipdpb.InstanceMetadata) []string {
   167  	out := make([]string, len(md))
   168  	for i, d := range md {
   169  		out[i] = d.Key
   170  	}
   171  	return out
   172  }