1// Copyright 2016 Google LLC 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// TODO(jba): test that OnError is getting called appropriately. 16 17package logadmin 18 19import ( 20 "context" 21 "flag" 22 "log" 23 "net/http" 24 "net/url" 25 "os" 26 "testing" 27 "time" 28 29 "cloud.google.com/go/internal/testutil" 30 "cloud.google.com/go/logging" 31 ltesting "cloud.google.com/go/logging/internal/testing" 32 "github.com/golang/protobuf/proto" 33 "github.com/golang/protobuf/ptypes" 34 durpb "github.com/golang/protobuf/ptypes/duration" 35 structpb "github.com/golang/protobuf/ptypes/struct" 36 "github.com/google/go-cmp/cmp/cmpopts" 37 "google.golang.org/api/option" 38 mrpb "google.golang.org/genproto/googleapis/api/monitoredres" 39 audit "google.golang.org/genproto/googleapis/cloud/audit" 40 logtypepb "google.golang.org/genproto/googleapis/logging/type" 41 logpb "google.golang.org/genproto/googleapis/logging/v2" 42 "google.golang.org/grpc" 43) 44 45var ( 46 client *Client 47 testProjectID string 48) 49 50var ( 51 // If true, this test is using the production service, not a fake. 52 integrationTest bool 53 54 newClient func(ctx context.Context, projectID string) *Client 55) 56 57func TestMain(m *testing.M) { 58 flag.Parse() // needed for testing.Short() 59 ctx := context.Background() 60 testProjectID = testutil.ProjID() 61 if testProjectID == "" || testing.Short() { 62 integrationTest = false 63 if testProjectID != "" { 64 log.Print("Integration tests skipped in short mode (using fake instead)") 65 } 66 testProjectID = "PROJECT_ID" 67 addr, err := ltesting.NewServer() 68 if err != nil { 69 log.Fatalf("creating fake server: %v", err) 70 } 71 newClient = func(ctx context.Context, projectID string) *Client { 72 conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock()) 73 if err != nil { 74 log.Fatalf("dialing %q: %v", addr, err) 75 } 76 c, err := NewClient(ctx, projectID, option.WithGRPCConn(conn)) 77 if err != nil { 78 log.Fatalf("creating client for fake at %q: %v", addr, err) 79 } 80 return c 81 } 82 } else { 83 integrationTest = true 84 ts := testutil.TokenSource(ctx, logging.AdminScope) 85 if ts == nil { 86 log.Fatal("The project key must be set. See CONTRIBUTING.md for details") 87 } 88 log.Printf("running integration tests with project %s", testProjectID) 89 newClient = func(ctx context.Context, projectID string) *Client { 90 c, err := NewClient(ctx, projectID, option.WithTokenSource(ts), 91 option.WithGRPCDialOption(grpc.WithBlock())) 92 if err != nil { 93 log.Fatalf("creating prod client: %v", err) 94 } 95 return c 96 } 97 } 98 client = newClient(ctx, testProjectID) 99 initMetrics(ctx) 100 cleanup := initSinks(ctx) 101 exit := m.Run() 102 cleanup() 103 client.Close() 104 os.Exit(exit) 105} 106 107// EntryIterator and DeleteLog are tested in the logging package. 108 109func TestClientClose(t *testing.T) { 110 c := newClient(context.Background(), testProjectID) 111 if err := c.Close(); err != nil { 112 t.Errorf("want got %v, want nil", err) 113 } 114} 115 116func TestFromLogEntry(t *testing.T) { 117 now := time.Now() 118 res := &mrpb.MonitoredResource{Type: "global"} 119 ts, err := ptypes.TimestampProto(now) 120 if err != nil { 121 t.Fatal(err) 122 } 123 logEntry := logpb.LogEntry{ 124 LogName: "projects/PROJECT_ID/logs/LOG_ID", 125 Resource: res, 126 Payload: &logpb.LogEntry_TextPayload{TextPayload: "hello"}, 127 Timestamp: ts, 128 Severity: logtypepb.LogSeverity_INFO, 129 InsertId: "123", 130 HttpRequest: &logtypepb.HttpRequest{ 131 RequestMethod: "GET", 132 RequestUrl: "http:://example.com/path?q=1", 133 RequestSize: 100, 134 Status: 200, 135 ResponseSize: 25, 136 Latency: &durpb.Duration{Seconds: 100}, 137 UserAgent: "user-agent", 138 RemoteIp: "127.0.0.1", 139 ServerIp: "127.0.0.1", 140 Referer: "referer", 141 CacheLookup: true, 142 CacheHit: true, 143 CacheValidatedWithOriginServer: true, 144 CacheFillBytes: 2048, 145 }, 146 Labels: map[string]string{ 147 "a": "1", 148 "b": "two", 149 "c": "true", 150 }, 151 SourceLocation: &logpb.LogEntrySourceLocation{ 152 File: "some_file.go", 153 Line: 1, 154 Function: "someFunction", 155 }, 156 } 157 u, err := url.Parse("http:://example.com/path?q=1") 158 if err != nil { 159 t.Fatal(err) 160 } 161 want := &logging.Entry{ 162 LogName: "projects/PROJECT_ID/logs/LOG_ID", 163 Resource: res, 164 Timestamp: now.In(time.UTC), 165 Severity: logging.Info, 166 Payload: "hello", 167 Labels: map[string]string{ 168 "a": "1", 169 "b": "two", 170 "c": "true", 171 }, 172 InsertID: "123", 173 HTTPRequest: &logging.HTTPRequest{ 174 Request: &http.Request{ 175 Method: "GET", 176 URL: u, 177 Header: map[string][]string{ 178 "User-Agent": {"user-agent"}, 179 "Referer": {"referer"}, 180 }, 181 }, 182 RequestSize: 100, 183 Status: 200, 184 ResponseSize: 25, 185 Latency: 100 * time.Second, 186 LocalIP: "127.0.0.1", 187 RemoteIP: "127.0.0.1", 188 CacheLookup: true, 189 CacheHit: true, 190 CacheValidatedWithOriginServer: true, 191 CacheFillBytes: 2048, 192 }, 193 SourceLocation: &logpb.LogEntrySourceLocation{ 194 File: "some_file.go", 195 Line: 1, 196 Function: "someFunction", 197 }, 198 } 199 got, err := fromLogEntry(&logEntry) 200 if err != nil { 201 t.Fatal(err) 202 } 203 if diff := testutil.Diff(got, want, cmpopts.IgnoreUnexported(http.Request{})); diff != "" { 204 t.Errorf("FullEntry:\n%s", diff) 205 } 206 207 // Proto payload. 208 alog := &audit.AuditLog{ 209 ServiceName: "svc", 210 MethodName: "method", 211 ResourceName: "shelves/S/books/B", 212 } 213 any, err := ptypes.MarshalAny(alog) 214 if err != nil { 215 t.Fatal(err) 216 } 217 logEntry = logpb.LogEntry{ 218 LogName: "projects/PROJECT_ID/logs/LOG_ID", 219 Resource: res, 220 Timestamp: ts, 221 Payload: &logpb.LogEntry_ProtoPayload{ProtoPayload: any}, 222 } 223 got, err = fromLogEntry(&logEntry) 224 if err != nil { 225 t.Fatal(err) 226 } 227 if !ltesting.PayloadEqual(got.Payload, alog) { 228 t.Errorf("got %+v, want %+v", got.Payload, alog) 229 } 230 231 // JSON payload. 232 jstruct := &structpb.Struct{Fields: map[string]*structpb.Value{ 233 "f": {Kind: &structpb.Value_NumberValue{NumberValue: 3.1}}, 234 }} 235 logEntry = logpb.LogEntry{ 236 LogName: "projects/PROJECT_ID/logs/LOG_ID", 237 Resource: res, 238 Timestamp: ts, 239 Payload: &logpb.LogEntry_JsonPayload{JsonPayload: jstruct}, 240 } 241 got, err = fromLogEntry(&logEntry) 242 if err != nil { 243 t.Fatal(err) 244 } 245 if !ltesting.PayloadEqual(got.Payload, jstruct) { 246 t.Errorf("got %+v, want %+v", got.Payload, jstruct) 247 } 248 249 // No payload. 250 logEntry = logpb.LogEntry{ 251 LogName: "projects/PROJECT_ID/logs/LOG_ID", 252 Resource: res, 253 Timestamp: ts, 254 } 255 got, err = fromLogEntry(&logEntry) 256 if err != nil { 257 t.Fatal(err) 258 } 259 if !ltesting.PayloadEqual(got.Payload, nil) { 260 t.Errorf("got %+v, want %+v", got.Payload, nil) 261 } 262} 263 264func TestListLogEntriesRequest(t *testing.T) { 265 dayAgo := time.Now().Add(-24 * time.Hour).UTC().Format(time.RFC3339) 266 for _, test := range []struct { 267 opts []EntriesOption 268 resourceNames []string 269 filter string 270 orderBy string 271 }{ 272 // Default is client's project ID, 24 hour lookback, and orderBy. 273 {nil, []string{"projects/PROJECT_ID"}, `timestamp >= "` + dayAgo + `"`, ""}, 274 // Timestamp default does not override user's filter 275 {[]EntriesOption{NewestFirst(), Filter(`timestamp > "2020-10-30T15:39:09Z"`)}, 276 []string{"projects/PROJECT_ID"}, `timestamp > "2020-10-30T15:39:09Z"`, "timestamp desc"}, 277 {[]EntriesOption{NewestFirst(), Filter("f")}, 278 []string{"projects/PROJECT_ID"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"}, 279 {[]EntriesOption{ProjectIDs([]string{"foo"})}, 280 []string{"projects/foo"}, `timestamp >= "` + dayAgo + `"`, ""}, 281 {[]EntriesOption{ResourceNames([]string{"folders/F", "organizations/O"})}, 282 []string{"folders/F", "organizations/O"}, `timestamp >= "` + dayAgo + `"`, ""}, 283 {[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})}, 284 []string{"projects/foo"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"}, 285 {[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})}, 286 []string{"projects/foo"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"}, 287 // If there are repeats, last one wins. 288 {[]EntriesOption{NewestFirst(), Filter("no"), ProjectIDs([]string{"foo"}), Filter("f")}, 289 []string{"projects/foo"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"}, 290 } { 291 got := listLogEntriesRequest("projects/PROJECT_ID", test.opts) 292 want := &logpb.ListLogEntriesRequest{ 293 ResourceNames: test.resourceNames, 294 Filter: test.filter, 295 OrderBy: test.orderBy, 296 } 297 if !proto.Equal(got, want) { 298 t.Errorf("%v:\ngot %v\nwant %v", test.opts, got, want) 299 } 300 } 301} 302