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