blob: fbe57154287d71929a34a536ee4a4199efd78ded [file] [log] [blame]
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package logging
import (
"errors"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"cloud.google.com/go/internal/testutil"
"google.golang.org/api/option"
)
func TestLogPayload(t *testing.T) {
lt := newLogTest(t)
defer lt.ts.Close()
tests := []struct {
name string
entry Entry
want string
}{
{
name: "string",
entry: Entry{
Time: time.Unix(0, 0),
Payload: "some log string",
},
want: `{"entries":[{"metadata":{"serviceName":"custom.googleapis.com","timestamp":"1970-01-01T00:00:00Z"},"textPayload":"some log string"}]}`,
},
{
name: "[]byte",
entry: Entry{
Time: time.Unix(0, 0),
Payload: []byte("some log bytes"),
},
want: `{"entries":[{"metadata":{"serviceName":"custom.googleapis.com","timestamp":"1970-01-01T00:00:00Z"},"textPayload":"some log bytes"}]}`,
},
{
name: "struct",
entry: Entry{
Time: time.Unix(0, 0),
Payload: struct {
Foo string `json:"foo"`
Bar int `json:"bar,omitempty"`
}{
Foo: "foovalue",
},
},
want: `{"entries":[{"metadata":{"serviceName":"custom.googleapis.com","timestamp":"1970-01-01T00:00:00Z"},"structPayload":{"foo":"foovalue"}}]}`,
},
{
name: "map[string]interface{}",
entry: Entry{
Time: time.Unix(0, 0),
Payload: map[string]interface{}{
"string": "foo",
"int": 42,
},
},
want: `{"entries":[{"metadata":{"serviceName":"custom.googleapis.com","timestamp":"1970-01-01T00:00:00Z"},"structPayload":{"int":42,"string":"foo"}}]}`,
},
{
name: "map[string]interface{}",
entry: Entry{
Time: time.Unix(0, 0),
Payload: customJSONObject{},
},
want: `{"entries":[{"metadata":{"serviceName":"custom.googleapis.com","timestamp":"1970-01-01T00:00:00Z"},"structPayload":{"custom":"json"}}]}`,
},
}
for _, tt := range tests {
lt.startGetRequest()
if err := lt.c.LogSync(tt.entry); err != nil {
t.Errorf("%s: LogSync = %v", tt.name, err)
continue
}
got := lt.getRequest()
if got != tt.want {
t.Errorf("%s: mismatch\n got: %s\nwant: %s\n", tt.name, got, tt.want)
}
}
}
func TestBufferInterval(t *testing.T) {
lt := newLogTest(t)
defer lt.ts.Close()
lt.c.CommonLabels = map[string]string{
"common1": "one",
"common2": "two",
}
lt.c.BufferInterval = 1 * time.Millisecond // immediately, basically.
lt.c.FlushAfter = 100 // but we'll only send 1
lt.startGetRequest()
lt.c.Logger(Debug).Printf("log line 1")
got := lt.getRequest()
want := `{"commonLabels":{"common1":"one","common2":"two"},"entries":[{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:01Z"},"textPayload":"log line 1\n"}]}`
if got != want {
t.Errorf(" got: %s\nwant: %s\n", got, want)
}
}
func TestFlushAfter(t *testing.T) {
lt := newLogTest(t)
defer lt.ts.Close()
lt.c.CommonLabels = map[string]string{
"common1": "one",
"common2": "two",
}
lt.c.BufferInterval = getRequestTimeout * 2
lt.c.FlushAfter = 2
lt.c.Logger(Debug).Printf("log line 1")
lt.startGetRequest()
lt.c.Logger(Debug).Printf("log line 2")
got := lt.getRequest()
want := `{"commonLabels":{"common1":"one","common2":"two"},"entries":[{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:01Z"},"textPayload":"log line 1\n"},{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:02Z"},"textPayload":"log line 2\n"}]}`
if got != want {
t.Errorf(" got: %s\nwant: %s\n", got, want)
}
}
func TestFlush(t *testing.T) {
lt := newLogTest(t)
defer lt.ts.Close()
lt.c.BufferInterval = getRequestTimeout * 2
lt.c.FlushAfter = 100 // but we'll only send 1, requiring a Flush
lt.c.Logger(Debug).Printf("log line 1")
lt.startGetRequest()
if err := lt.c.Flush(); err != nil {
t.Fatal(err)
}
got := lt.getRequest()
want := `{"entries":[{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:01Z"},"textPayload":"log line 1\n"}]}`
if got != want {
t.Errorf(" got: %s\nwant: %s\n", got, want)
}
}
func TestOverflow(t *testing.T) {
lt := newLogTest(t)
defer lt.ts.Close()
lt.c.FlushAfter = 1
lt.c.BufferLimit = 5
lt.c.BufferInterval = 1 * time.Millisecond // immediately, basically.
someErr := errors.New("some specific error value")
lt.c.Overflow = func(c *Client, e Entry) error {
return someErr
}
unblock := make(chan bool, 1)
inHandler := make(chan bool, 1)
lt.handlerc <- http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inHandler <- true
<-unblock
ioutil.ReadAll(r.Body)
io.WriteString(w, "{}") // WriteLogEntriesResponse
})
lt.c.Logger(Debug).Printf("log line 1")
<-inHandler
lt.c.Logger(Debug).Printf("log line 2")
lt.c.Logger(Debug).Printf("log line 3")
lt.c.Logger(Debug).Printf("log line 4")
lt.c.Logger(Debug).Printf("log line 5")
queued, inFlight := lt.c.stats()
if want := 4; queued != want {
t.Errorf("queued = %d; want %d", queued, want)
}
if want := 1; inFlight != want {
t.Errorf("inFlight = %d; want %d", inFlight, want)
}
if err := lt.c.Log(Entry{Payload: "to overflow"}); err != someErr {
t.Errorf("Log(overflow Log entry) = %v; want someErr", err)
}
lt.startGetRequest()
unblock <- true
got := lt.getRequest()
want := `{"entries":[{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:02Z"},"textPayload":"log line 2\n"},{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:03Z"},"textPayload":"log line 3\n"},{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:04Z"},"textPayload":"log line 4\n"},{"metadata":{"serviceName":"custom.googleapis.com","severity":"DEBUG","timestamp":"1970-01-01T00:00:05Z"},"textPayload":"log line 5\n"}]}`
if got != want {
t.Errorf(" got: %s\nwant: %s\n", got, want)
}
if err := lt.c.Flush(); err != nil {
t.Fatal(err)
}
queued, inFlight = lt.c.stats()
if want := 0; queued != want {
t.Errorf("queued = %d; want %d", queued, want)
}
if want := 0; inFlight != want {
t.Errorf("inFlight = %d; want %d", inFlight, want)
}
}
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Integration tests skipped in short mode")
}
ctx := context.Background()
ts := testutil.TokenSource(ctx, Scope)
if ts == nil {
t.Skip("Integration tests skipped. See CONTRIBUTING.md for details")
}
projID := testutil.ProjID()
c, err := NewClient(ctx, projID, "logging-integration-test", option.WithTokenSource(ts))
if err != nil {
t.Fatalf("error creating client: %v", err)
}
if err := c.Ping(); err != nil {
t.Fatalf("error pinging logging api: %v", err)
}
// Ping twice, to verify that deduping doesn't change the result.
if err := c.Ping(); err != nil {
t.Fatalf("error pinging logging api: %v", err)
}
if err := c.LogSync(Entry{Payload: customJSONObject{}}); err != nil {
t.Fatalf("error writing log: %v", err)
}
if err := c.Log(Entry{Payload: customJSONObject{}}); err != nil {
t.Fatalf("error writing log: %v", err)
}
if _, err := c.Writer(Default).Write([]byte("test log with io.Writer")); err != nil {
t.Fatalf("error writing log using io.Writer: %v", err)
}
c.Logger(Default).Println("test log with log.Logger")
if err := c.Flush(); err != nil {
t.Fatalf("error flushing logs: %v", err)
}
}
func TestIntegrationPingBadProject(t *testing.T) {
if testing.Short() {
t.Skip("Integration tests skipped in short mode")
}
ctx := context.Background()
ts := testutil.TokenSource(ctx, Scope)
if ts == nil {
t.Skip("Integration tests skipped. See CONTRIBUTING.md for details")
}
for _, projID := range []string{
testutil.ProjID() + "-BAD", // nonexistent project
"amazing-height-519", // exists, but wrong creds
} {
c, err := NewClient(ctx, projID, "logging-integration-test", option.WithTokenSource(ts))
if err != nil {
t.Fatalf("project %s: error creating client: %v", projID, err)
}
if err := c.Ping(); err == nil {
t.Errorf("project %s: want error pinging logging api, got nil", projID)
}
// Ping twice, just to make sure the deduping doesn't mess with the result.
if err := c.Ping(); err == nil {
t.Errorf("project %s: want error pinging logging api, got nil", projID)
}
}
}
func (c *Client) stats() (queued, inFlight int) {
c.mu.Lock()
defer c.mu.Unlock()
return len(c.queued), c.inFlight
}
type customJSONObject struct{}
func (customJSONObject) MarshalJSON() ([]byte, error) {
return []byte(`{"custom":"json"}`), nil
}
type logTest struct {
t *testing.T
ts *httptest.Server
c *Client
handlerc chan<- http.Handler
bodyc chan string
}
func newLogTest(t *testing.T) *logTest {
handlerc := make(chan http.Handler, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case h := <-handlerc:
h.ServeHTTP(w, r)
default:
slurp, _ := ioutil.ReadAll(r.Body)
t.Errorf("Unexpected HTTP request received: %s", slurp)
w.WriteHeader(500)
io.WriteString(w, "unexpected HTTP request")
}
}))
c, err := NewClient(context.Background(), "PROJ-ID", "LOG-NAME",
option.WithEndpoint(ts.URL),
option.WithTokenSource(dummyTokenSource{}), // prevent DefaultTokenSource
)
if err != nil {
t.Fatal(err)
}
var clock struct {
sync.Mutex
now time.Time
}
c.timeNow = func() time.Time {
clock.Lock()
defer clock.Unlock()
if clock.now.IsZero() {
clock.now = time.Unix(0, 0)
}
clock.now = clock.now.Add(1 * time.Second)
return clock.now
}
return &logTest{
t: t,
ts: ts,
c: c,
handlerc: handlerc,
}
}
func (lt *logTest) startGetRequest() {
bodyc := make(chan string, 1)
lt.bodyc = bodyc
lt.handlerc <- http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slurp, err := ioutil.ReadAll(r.Body)
if err != nil {
bodyc <- "ERROR: " + err.Error()
} else {
bodyc <- string(slurp)
}
io.WriteString(w, "{}") // a complete WriteLogEntriesResponse JSON struct
})
}
const getRequestTimeout = 5 * time.Second
func (lt *logTest) getRequest() string {
if lt.bodyc == nil {
lt.t.Fatalf("getRequest called without previous startGetRequest")
}
select {
case v := <-lt.bodyc:
return strings.TrimSpace(v)
case <-time.After(getRequestTimeout):
lt.t.Fatalf("timeout waiting for request")
panic("unreachable")
}
}
// dummyTokenSource returns fake oauth2 tokens for local testing.
type dummyTokenSource struct{}
func (dummyTokenSource) Token() (*oauth2.Token, error) {
return new(oauth2.Token), nil
}