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 }