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// Tests that require access to unexported names of the logging package. 16 17package logging 18 19import ( 20 "encoding/json" 21 "net/http" 22 "net/url" 23 "testing" 24 "time" 25 26 "cloud.google.com/go/internal/testutil" 27 "github.com/golang/protobuf/proto" 28 durpb "github.com/golang/protobuf/ptypes/duration" 29 structpb "github.com/golang/protobuf/ptypes/struct" 30 "google.golang.org/api/logging/v2" 31 "google.golang.org/api/support/bundler" 32 mrpb "google.golang.org/genproto/googleapis/api/monitoredres" 33 logtypepb "google.golang.org/genproto/googleapis/logging/type" 34) 35 36func TestLoggerCreation(t *testing.T) { 37 const logID = "testing" 38 c := &Client{parent: "projects/PROJECT_ID"} 39 customResource := &mrpb.MonitoredResource{ 40 Type: "global", 41 Labels: map[string]string{ 42 "project_id": "ANOTHER_PROJECT", 43 }, 44 } 45 defaultBundler := &bundler.Bundler{ 46 DelayThreshold: DefaultDelayThreshold, 47 BundleCountThreshold: DefaultEntryCountThreshold, 48 BundleByteThreshold: DefaultEntryByteThreshold, 49 BundleByteLimit: 0, 50 BufferedByteLimit: DefaultBufferedByteLimit, 51 } 52 for _, test := range []struct { 53 options []LoggerOption 54 wantLogger *Logger 55 defaultResource bool 56 wantBundler *bundler.Bundler 57 }{ 58 { 59 options: nil, 60 wantLogger: &Logger{}, 61 defaultResource: true, 62 wantBundler: defaultBundler, 63 }, 64 { 65 options: []LoggerOption{ 66 CommonResource(nil), 67 CommonLabels(map[string]string{"a": "1"}), 68 }, 69 wantLogger: &Logger{ 70 commonResource: nil, 71 commonLabels: map[string]string{"a": "1"}, 72 }, 73 wantBundler: defaultBundler, 74 }, 75 { 76 options: []LoggerOption{CommonResource(customResource)}, 77 wantLogger: &Logger{commonResource: customResource}, 78 wantBundler: defaultBundler, 79 }, 80 { 81 options: []LoggerOption{ 82 DelayThreshold(time.Minute), 83 EntryCountThreshold(99), 84 EntryByteThreshold(17), 85 EntryByteLimit(18), 86 BufferedByteLimit(19), 87 }, 88 wantLogger: &Logger{}, 89 defaultResource: true, 90 wantBundler: &bundler.Bundler{ 91 DelayThreshold: time.Minute, 92 BundleCountThreshold: 99, 93 BundleByteThreshold: 17, 94 BundleByteLimit: 18, 95 BufferedByteLimit: 19, 96 }, 97 }, 98 } { 99 gotLogger := c.Logger(logID, test.options...) 100 if got, want := gotLogger.commonResource, test.wantLogger.commonResource; !test.defaultResource && !proto.Equal(got, want) { 101 t.Errorf("%v: resource: got %v, want %v", test.options, got, want) 102 } 103 if got, want := gotLogger.commonLabels, test.wantLogger.commonLabels; !testutil.Equal(got, want) { 104 t.Errorf("%v: commonLabels: got %v, want %v", test.options, got, want) 105 } 106 if got, want := gotLogger.bundler.DelayThreshold, test.wantBundler.DelayThreshold; got != want { 107 t.Errorf("%v: DelayThreshold: got %v, want %v", test.options, got, want) 108 } 109 if got, want := gotLogger.bundler.BundleCountThreshold, test.wantBundler.BundleCountThreshold; got != want { 110 t.Errorf("%v: BundleCountThreshold: got %v, want %v", test.options, got, want) 111 } 112 if got, want := gotLogger.bundler.BundleByteThreshold, test.wantBundler.BundleByteThreshold; got != want { 113 t.Errorf("%v: BundleByteThreshold: got %v, want %v", test.options, got, want) 114 } 115 if got, want := gotLogger.bundler.BundleByteLimit, test.wantBundler.BundleByteLimit; got != want { 116 t.Errorf("%v: BundleByteLimit: got %v, want %v", test.options, got, want) 117 } 118 if got, want := gotLogger.bundler.BufferedByteLimit, test.wantBundler.BufferedByteLimit; got != want { 119 t.Errorf("%v: BufferedByteLimit: got %v, want %v", test.options, got, want) 120 } 121 } 122} 123 124func TestToProtoStruct(t *testing.T) { 125 v := struct { 126 Foo string `json:"foo"` 127 Bar int `json:"bar,omitempty"` 128 Baz []float64 `json:"baz"` 129 Moo map[string]interface{} `json:"moo"` 130 }{ 131 Foo: "foovalue", 132 Baz: []float64{1.1}, 133 Moo: map[string]interface{}{ 134 "a": 1, 135 "b": "two", 136 "c": true, 137 }, 138 } 139 140 got, err := toProtoStruct(v) 141 if err != nil { 142 t.Fatal(err) 143 } 144 want := &structpb.Struct{ 145 Fields: map[string]*structpb.Value{ 146 "foo": {Kind: &structpb.Value_StringValue{StringValue: v.Foo}}, 147 "baz": {Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{ 148 {Kind: &structpb.Value_NumberValue{NumberValue: 1.1}}, 149 }}}}, 150 "moo": {Kind: &structpb.Value_StructValue{ 151 StructValue: &structpb.Struct{ 152 Fields: map[string]*structpb.Value{ 153 "a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}}, 154 "b": {Kind: &structpb.Value_StringValue{StringValue: "two"}}, 155 "c": {Kind: &structpb.Value_BoolValue{BoolValue: true}}, 156 }, 157 }, 158 }}, 159 }, 160 } 161 if !proto.Equal(got, want) { 162 t.Errorf("got %+v\nwant %+v", got, want) 163 } 164 165 // Non-structs should fail to convert. 166 for v := range []interface{}{3, "foo", []int{1, 2, 3}} { 167 _, err := toProtoStruct(v) 168 if err == nil { 169 t.Errorf("%v: got nil, want error", v) 170 } 171 } 172 173 // Test fast path. 174 got, err = toProtoStruct(want) 175 if err != nil { 176 t.Fatal(err) 177 } 178 if got != want { 179 t.Error("got and want should be identical, but are not") 180 } 181} 182 183func TestToLogEntryPayload(t *testing.T) { 184 var logger Logger 185 for _, test := range []struct { 186 in interface{} 187 wantText string 188 wantStruct *structpb.Struct 189 }{ 190 { 191 in: "string", 192 wantText: "string", 193 }, 194 { 195 in: map[string]interface{}{"a": 1, "b": true}, 196 wantStruct: &structpb.Struct{ 197 Fields: map[string]*structpb.Value{ 198 "a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}}, 199 "b": {Kind: &structpb.Value_BoolValue{BoolValue: true}}, 200 }, 201 }, 202 }, 203 { 204 in: json.RawMessage([]byte(`{"a": 1, "b": true}`)), 205 wantStruct: &structpb.Struct{ 206 Fields: map[string]*structpb.Value{ 207 "a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}}, 208 "b": {Kind: &structpb.Value_BoolValue{BoolValue: true}}, 209 }, 210 }, 211 }, 212 } { 213 e, err := logger.toLogEntry(Entry{Payload: test.in}) 214 if err != nil { 215 t.Fatalf("%+v: %v", test.in, err) 216 } 217 if test.wantStruct != nil { 218 got := e.GetJsonPayload() 219 if !proto.Equal(got, test.wantStruct) { 220 t.Errorf("%+v: got %s, want %s", test.in, got, test.wantStruct) 221 } 222 } else { 223 got := e.GetTextPayload() 224 if got != test.wantText { 225 t.Errorf("%+v: got %s, want %s", test.in, got, test.wantText) 226 } 227 } 228 } 229} 230 231func TestToLogEntryTrace(t *testing.T) { 232 logger := &Logger{client: &Client{parent: "projects/P"}} 233 // Verify that we get the trace from the HTTP request if it isn't 234 // provided by the caller. 235 u := &url.URL{Scheme: "http"} 236 237 tests := []struct { 238 name string 239 in Entry 240 want logging.LogEntry 241 }{ 242 {"BlankLogEntry", Entry{}, logging.LogEntry{}}, 243 {"Already set Trace", Entry{Trace: "t1"}, logging.LogEntry{Trace: "t1"}}, 244 { 245 "No X-Trace-Context header", 246 Entry{ 247 HTTPRequest: &HTTPRequest{ 248 Request: &http.Request{URL: u, Header: http.Header{"foo": {"bar"}}}, 249 }, 250 }, 251 logging.LogEntry{}, 252 }, 253 { 254 "X-Trace-Context header with all fields", 255 Entry{ 256 TraceSampled: false, 257 HTTPRequest: &HTTPRequest{ 258 Request: &http.Request{ 259 URL: u, 260 Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/000000000000004a;o=1"}}, 261 }, 262 }, 263 }, 264 logging.LogEntry{Trace: "projects/P/traces/105445aa7843bc8bf206b120001000", SpanId: "000000000000004a", TraceSampled: true}, 265 }, 266 { 267 "X-Trace-Context header with all fields; TraceSampled explicitly set", 268 Entry{ 269 TraceSampled: true, 270 HTTPRequest: &HTTPRequest{ 271 Request: &http.Request{ 272 URL: u, 273 Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/000000000000004a;o=0"}}, 274 }, 275 }, 276 }, 277 logging.LogEntry{Trace: "projects/P/traces/105445aa7843bc8bf206b120001000", SpanId: "000000000000004a", TraceSampled: true}, 278 }, 279 { 280 "X-Trace-Context header with all fields; TraceSampled from Header", 281 Entry{ 282 HTTPRequest: &HTTPRequest{ 283 Request: &http.Request{ 284 URL: u, 285 Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/000000000000004a;o=1"}}, 286 }, 287 }, 288 }, 289 logging.LogEntry{Trace: "projects/P/traces/105445aa7843bc8bf206b120001000", SpanId: "000000000000004a", TraceSampled: true}, 290 }, 291 { 292 "X-Trace-Context header with blank trace", 293 Entry{ 294 HTTPRequest: &HTTPRequest{ 295 Request: &http.Request{ 296 URL: u, 297 Header: http.Header{"X-Cloud-Trace-Context": {"/0;o=1"}}, 298 }, 299 }, 300 }, 301 logging.LogEntry{TraceSampled: true}, 302 }, 303 { 304 "X-Trace-Context header with blank span", 305 Entry{ 306 HTTPRequest: &HTTPRequest{ 307 Request: &http.Request{ 308 URL: u, 309 Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/;o=0"}}, 310 }, 311 }, 312 }, 313 logging.LogEntry{Trace: "projects/P/traces/105445aa7843bc8bf206b120001000"}, 314 }, 315 { 316 "X-Trace-Context header with missing traceSampled aka ?o=*", 317 Entry{ 318 HTTPRequest: &HTTPRequest{ 319 Request: &http.Request{ 320 URL: u, 321 Header: http.Header{"X-Cloud-Trace-Context": {"105445aa7843bc8bf206b120001000/0"}}, 322 }, 323 }, 324 }, 325 logging.LogEntry{Trace: "projects/P/traces/105445aa7843bc8bf206b120001000"}, 326 }, 327 { 328 "X-Trace-Context header with all blank fields", 329 Entry{ 330 HTTPRequest: &HTTPRequest{ 331 Request: &http.Request{ 332 URL: u, 333 Header: http.Header{"X-Cloud-Trace-Context": {""}}, 334 }, 335 }, 336 }, 337 logging.LogEntry{}, 338 }, 339 { 340 "Invalid X-Trace-Context header but already set TraceID", 341 Entry{ 342 HTTPRequest: &HTTPRequest{ 343 Request: &http.Request{ 344 URL: u, 345 Header: http.Header{"X-Cloud-Trace-Context": {"t3"}}, 346 }, 347 }, 348 Trace: "t4", 349 }, 350 logging.LogEntry{Trace: "t4"}, 351 }, 352 { 353 "Already set TraceID and SpanID", 354 Entry{Trace: "t1", SpanID: "007"}, 355 logging.LogEntry{Trace: "t1", SpanId: "007"}, 356 }, 357 } 358 359 for _, test := range tests { 360 t.Run(test.name, func(t *testing.T) { 361 e, err := logger.toLogEntry(test.in) 362 if err != nil { 363 t.Fatalf("Unexpected error:: %+v: %v", test.in, err) 364 } 365 if got := e.Trace; got != test.want.Trace { 366 t.Errorf("TraceId: %+v: got %q, want %q", test.in, got, test.want.Trace) 367 } 368 if got := e.SpanId; got != test.want.SpanId { 369 t.Errorf("SpanId: %+v: got %q, want %q", test.in, got, test.want.SpanId) 370 } 371 if got := e.TraceSampled; got != test.want.TraceSampled { 372 t.Errorf("TraceSampled: %+v: got %t, want %t", test.in, got, test.want.TraceSampled) 373 } 374 }) 375 } 376} 377 378func TestFromHTTPRequest(t *testing.T) { 379 // The test URL has invalid UTF-8 runes. 380 const testURL = "http://example.com/path?q=1&name=\xfe\xff" 381 u, err := url.Parse(testURL) 382 if err != nil { 383 t.Fatal(err) 384 } 385 req := &HTTPRequest{ 386 Request: &http.Request{ 387 Method: "GET", 388 URL: u, 389 Header: map[string][]string{ 390 "User-Agent": {"user-agent"}, 391 "Referer": {"referer"}, 392 }, 393 }, 394 RequestSize: 100, 395 Status: 200, 396 ResponseSize: 25, 397 Latency: 100 * time.Second, 398 LocalIP: "127.0.0.1", 399 RemoteIP: "10.0.1.1", 400 CacheHit: true, 401 CacheValidatedWithOriginServer: true, 402 } 403 got, err := fromHTTPRequest(req) 404 if err != nil { 405 t.Errorf("got %v", err) 406 } 407 want := &logtypepb.HttpRequest{ 408 RequestMethod: "GET", 409 410 // RequestUrl should have its invalid utf-8 runes replaced by the Unicode replacement character U+FFFD. 411 // See Issue https://github.com/googleapis/google-cloud-go/issues/1383 412 RequestUrl: "http://example.com/path?q=1&name=" + string('\ufffd') + string('\ufffd'), 413 414 RequestSize: 100, 415 Status: 200, 416 ResponseSize: 25, 417 Latency: &durpb.Duration{Seconds: 100}, 418 UserAgent: "user-agent", 419 ServerIp: "127.0.0.1", 420 RemoteIp: "10.0.1.1", 421 Referer: "referer", 422 CacheHit: true, 423 CacheValidatedWithOriginServer: true, 424 } 425 if !proto.Equal(got, want) { 426 t.Errorf("got %+v\nwant %+v", got, want) 427 } 428 429 // And finally checks directly that the error that was 430 // in https://github.com/googleapis/google-cloud-go/issues/1383 431 // doesn't not regress. 432 if _, err := proto.Marshal(got); err != nil { 433 t.Fatalf("Unexpected proto.Marshal error: %v", err) 434 } 435 436 // fromHTTPRequest returns nil if there is no Request property (but does not panic) 437 reqNil := &HTTPRequest{ 438 RequestSize: 100, 439 } 440 got, err = fromHTTPRequest(reqNil) 441 if got != nil && err == nil { 442 t.Errorf("got %+v\nwant %+v", got, want) 443 } 444} 445 446func TestMonitoredResource(t *testing.T) { 447 for _, test := range []struct { 448 parent string 449 want *mrpb.MonitoredResource 450 }{ 451 { 452 "projects/P", 453 &mrpb.MonitoredResource{ 454 Type: "project", 455 Labels: map[string]string{"project_id": "P"}, 456 }, 457 }, 458 459 { 460 "folders/F", 461 &mrpb.MonitoredResource{ 462 Type: "folder", 463 Labels: map[string]string{"folder_id": "F"}, 464 }, 465 }, 466 { 467 "billingAccounts/B", 468 &mrpb.MonitoredResource{ 469 Type: "billing_account", 470 Labels: map[string]string{"account_id": "B"}, 471 }, 472 }, 473 { 474 "organizations/123", 475 &mrpb.MonitoredResource{ 476 Type: "organization", 477 Labels: map[string]string{"organization_id": "123"}, 478 }, 479 }, 480 { 481 "unknown/X", 482 &mrpb.MonitoredResource{ 483 Type: "global", 484 Labels: map[string]string{"project_id": "X"}, 485 }, 486 }, 487 { 488 "whatever", 489 &mrpb.MonitoredResource{ 490 Type: "global", 491 Labels: map[string]string{"project_id": "whatever"}, 492 }, 493 }, 494 } { 495 got := monitoredResource(test.parent) 496 if !testutil.Equal(got, test.want) { 497 t.Errorf("%q: got %+v, want %+v", test.parent, got, test.want) 498 } 499 } 500} 501 502// Used by the tests in logging_test. 503func SetNow(f func() time.Time) { 504 now = f 505} 506