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/support/bundler"
31	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
32	logtypepb "google.golang.org/genproto/googleapis/logging/type"
33)
34
35func TestLoggerCreation(t *testing.T) {
36	const logID = "testing"
37	c := &Client{parent: "projects/PROJECT_ID"}
38	customResource := &mrpb.MonitoredResource{
39		Type: "global",
40		Labels: map[string]string{
41			"project_id": "ANOTHER_PROJECT",
42		},
43	}
44	defaultBundler := &bundler.Bundler{
45		DelayThreshold:       DefaultDelayThreshold,
46		BundleCountThreshold: DefaultEntryCountThreshold,
47		BundleByteThreshold:  DefaultEntryByteThreshold,
48		BundleByteLimit:      0,
49		BufferedByteLimit:    DefaultBufferedByteLimit,
50	}
51	for _, test := range []struct {
52		options         []LoggerOption
53		wantLogger      *Logger
54		defaultResource bool
55		wantBundler     *bundler.Bundler
56	}{
57		{
58			options:         nil,
59			wantLogger:      &Logger{},
60			defaultResource: true,
61			wantBundler:     defaultBundler,
62		},
63		{
64			options: []LoggerOption{
65				CommonResource(nil),
66				CommonLabels(map[string]string{"a": "1"}),
67			},
68			wantLogger: &Logger{
69				commonResource: nil,
70				commonLabels:   map[string]string{"a": "1"},
71			},
72			wantBundler: defaultBundler,
73		},
74		{
75			options:     []LoggerOption{CommonResource(customResource)},
76			wantLogger:  &Logger{commonResource: customResource},
77			wantBundler: defaultBundler,
78		},
79		{
80			options: []LoggerOption{
81				DelayThreshold(time.Minute),
82				EntryCountThreshold(99),
83				EntryByteThreshold(17),
84				EntryByteLimit(18),
85				BufferedByteLimit(19),
86			},
87			wantLogger:      &Logger{},
88			defaultResource: true,
89			wantBundler: &bundler.Bundler{
90				DelayThreshold:       time.Minute,
91				BundleCountThreshold: 99,
92				BundleByteThreshold:  17,
93				BundleByteLimit:      18,
94				BufferedByteLimit:    19,
95			},
96		},
97	} {
98		gotLogger := c.Logger(logID, test.options...)
99		if got, want := gotLogger.commonResource, test.wantLogger.commonResource; !test.defaultResource && !proto.Equal(got, want) {
100			t.Errorf("%v: resource: got %v, want %v", test.options, got, want)
101		}
102		if got, want := gotLogger.commonLabels, test.wantLogger.commonLabels; !testutil.Equal(got, want) {
103			t.Errorf("%v: commonLabels: got %v, want %v", test.options, got, want)
104		}
105		if got, want := gotLogger.bundler.DelayThreshold, test.wantBundler.DelayThreshold; got != want {
106			t.Errorf("%v: DelayThreshold: got %v, want %v", test.options, got, want)
107		}
108		if got, want := gotLogger.bundler.BundleCountThreshold, test.wantBundler.BundleCountThreshold; got != want {
109			t.Errorf("%v: BundleCountThreshold: got %v, want %v", test.options, got, want)
110		}
111		if got, want := gotLogger.bundler.BundleByteThreshold, test.wantBundler.BundleByteThreshold; got != want {
112			t.Errorf("%v: BundleByteThreshold: got %v, want %v", test.options, got, want)
113		}
114		if got, want := gotLogger.bundler.BundleByteLimit, test.wantBundler.BundleByteLimit; got != want {
115			t.Errorf("%v: BundleByteLimit: got %v, want %v", test.options, got, want)
116		}
117		if got, want := gotLogger.bundler.BufferedByteLimit, test.wantBundler.BufferedByteLimit; got != want {
118			t.Errorf("%v: BufferedByteLimit: got %v, want %v", test.options, got, want)
119		}
120	}
121}
122
123func TestToProtoStruct(t *testing.T) {
124	v := struct {
125		Foo string                 `json:"foo"`
126		Bar int                    `json:"bar,omitempty"`
127		Baz []float64              `json:"baz"`
128		Moo map[string]interface{} `json:"moo"`
129	}{
130		Foo: "foovalue",
131		Baz: []float64{1.1},
132		Moo: map[string]interface{}{
133			"a": 1,
134			"b": "two",
135			"c": true,
136		},
137	}
138
139	got, err := toProtoStruct(v)
140	if err != nil {
141		t.Fatal(err)
142	}
143	want := &structpb.Struct{
144		Fields: map[string]*structpb.Value{
145			"foo": {Kind: &structpb.Value_StringValue{StringValue: v.Foo}},
146			"baz": {Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{
147				{Kind: &structpb.Value_NumberValue{NumberValue: 1.1}},
148			}}}},
149			"moo": {Kind: &structpb.Value_StructValue{
150				StructValue: &structpb.Struct{
151					Fields: map[string]*structpb.Value{
152						"a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}},
153						"b": {Kind: &structpb.Value_StringValue{StringValue: "two"}},
154						"c": {Kind: &structpb.Value_BoolValue{BoolValue: true}},
155					},
156				},
157			}},
158		},
159	}
160	if !proto.Equal(got, want) {
161		t.Errorf("got  %+v\nwant %+v", got, want)
162	}
163
164	// Non-structs should fail to convert.
165	for v := range []interface{}{3, "foo", []int{1, 2, 3}} {
166		_, err := toProtoStruct(v)
167		if err == nil {
168			t.Errorf("%v: got nil, want error", v)
169		}
170	}
171
172	// Test fast path.
173	got, err = toProtoStruct(want)
174	if err != nil {
175		t.Fatal(err)
176	}
177	if got != want {
178		t.Error("got and want should be identical, but are not")
179	}
180}
181
182func TestToLogEntryPayload(t *testing.T) {
183	var logger Logger
184	for _, test := range []struct {
185		in         interface{}
186		wantText   string
187		wantStruct *structpb.Struct
188	}{
189		{
190			in:       "string",
191			wantText: "string",
192		},
193		{
194			in: map[string]interface{}{"a": 1, "b": true},
195			wantStruct: &structpb.Struct{
196				Fields: map[string]*structpb.Value{
197					"a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}},
198					"b": {Kind: &structpb.Value_BoolValue{BoolValue: true}},
199				},
200			},
201		},
202		{
203			in: json.RawMessage([]byte(`{"a": 1, "b": true}`)),
204			wantStruct: &structpb.Struct{
205				Fields: map[string]*structpb.Value{
206					"a": {Kind: &structpb.Value_NumberValue{NumberValue: 1}},
207					"b": {Kind: &structpb.Value_BoolValue{BoolValue: true}},
208				},
209			},
210		},
211	} {
212		e, err := logger.toLogEntry(Entry{Payload: test.in})
213		if err != nil {
214			t.Fatalf("%+v: %v", test.in, err)
215		}
216		if test.wantStruct != nil {
217			got := e.GetJsonPayload()
218			if !proto.Equal(got, test.wantStruct) {
219				t.Errorf("%+v: got %s, want %s", test.in, got, test.wantStruct)
220			}
221		} else {
222			got := e.GetTextPayload()
223			if got != test.wantText {
224				t.Errorf("%+v: got %s, want %s", test.in, got, test.wantText)
225			}
226		}
227	}
228}
229
230func TestToLogEntryTrace(t *testing.T) {
231	logger := &Logger{client: &Client{parent: "projects/P"}}
232	// Verify that we get the trace from the HTTP request if it isn't
233	// provided by the caller.
234	u := &url.URL{Scheme: "http"}
235	for _, test := range []struct {
236		in   Entry
237		want string
238	}{
239		{Entry{}, ""},
240		{Entry{Trace: "t1"}, "t1"},
241		{
242			Entry{
243				HTTPRequest: &HTTPRequest{
244					Request: &http.Request{URL: u, Header: http.Header{"foo": {"bar"}}},
245				},
246			},
247			"",
248		},
249		{
250			Entry{
251				HTTPRequest: &HTTPRequest{
252					Request: &http.Request{
253						URL:    u,
254						Header: http.Header{"X-Cloud-Trace-Context": {"t2"}},
255					},
256				},
257			},
258			"projects/P/traces/t2",
259		},
260		{
261			Entry{
262				HTTPRequest: &HTTPRequest{
263					Request: &http.Request{
264						URL:    u,
265						Header: http.Header{"X-Cloud-Trace-Context": {"t3"}},
266					},
267				},
268				Trace: "t4",
269			},
270			"t4",
271		},
272	} {
273		e, err := logger.toLogEntry(test.in)
274		if err != nil {
275			t.Fatalf("%+v: %v", test.in, err)
276		}
277		if got := e.Trace; got != test.want {
278			t.Errorf("%+v: got %q, want %q", test.in, got, test.want)
279		}
280	}
281}
282
283func TestFromHTTPRequest(t *testing.T) {
284	const testURL = "http:://example.com/path?q=1"
285	u, err := url.Parse(testURL)
286	if err != nil {
287		t.Fatal(err)
288	}
289	req := &HTTPRequest{
290		Request: &http.Request{
291			Method: "GET",
292			URL:    u,
293			Header: map[string][]string{
294				"User-Agent": {"user-agent"},
295				"Referer":    {"referer"},
296			},
297		},
298		RequestSize:                    100,
299		Status:                         200,
300		ResponseSize:                   25,
301		Latency:                        100 * time.Second,
302		LocalIP:                        "127.0.0.1",
303		RemoteIP:                       "10.0.1.1",
304		CacheHit:                       true,
305		CacheValidatedWithOriginServer: true,
306	}
307	got := fromHTTPRequest(req)
308	want := &logtypepb.HttpRequest{
309		RequestMethod:                  "GET",
310		RequestUrl:                     testURL,
311		RequestSize:                    100,
312		Status:                         200,
313		ResponseSize:                   25,
314		Latency:                        &durpb.Duration{Seconds: 100},
315		UserAgent:                      "user-agent",
316		ServerIp:                       "127.0.0.1",
317		RemoteIp:                       "10.0.1.1",
318		Referer:                        "referer",
319		CacheHit:                       true,
320		CacheValidatedWithOriginServer: true,
321	}
322	if !proto.Equal(got, want) {
323		t.Errorf("got  %+v\nwant %+v", got, want)
324	}
325}
326
327func TestMonitoredResource(t *testing.T) {
328	for _, test := range []struct {
329		parent string
330		want   *mrpb.MonitoredResource
331	}{
332		{
333			"projects/P",
334			&mrpb.MonitoredResource{
335				Type:   "project",
336				Labels: map[string]string{"project_id": "P"},
337			},
338		},
339
340		{
341			"folders/F",
342			&mrpb.MonitoredResource{
343				Type:   "folder",
344				Labels: map[string]string{"folder_id": "F"},
345			},
346		},
347		{
348			"billingAccounts/B",
349			&mrpb.MonitoredResource{
350				Type:   "billing_account",
351				Labels: map[string]string{"account_id": "B"},
352			},
353		},
354		{
355			"organizations/123",
356			&mrpb.MonitoredResource{
357				Type:   "organization",
358				Labels: map[string]string{"organization_id": "123"},
359			},
360		},
361		{
362			"unknown/X",
363			&mrpb.MonitoredResource{
364				Type:   "global",
365				Labels: map[string]string{"project_id": "X"},
366			},
367		},
368		{
369			"whatever",
370			&mrpb.MonitoredResource{
371				Type:   "global",
372				Labels: map[string]string{"project_id": "whatever"},
373			},
374		},
375	} {
376		got := monitoredResource(test.parent)
377		if !testutil.Equal(got, test.want) {
378			t.Errorf("%q: got %+v, want %+v", test.parent, got, test.want)
379		}
380	}
381}
382
383// Used by the tests in logging_test.
384func SetNow(f func() time.Time) {
385	now = f
386}
387