github.com/observiq/carbon@v0.9.11-0.20200820160507-1b872e368a5e/operator/builtin/output/google_cloud_test.go (about)

     1  package output
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"strconv"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	vkit "cloud.google.com/go/logging/apiv2"
    13  	"github.com/golang/protobuf/ptypes"
    14  	tspb "github.com/golang/protobuf/ptypes/timestamp"
    15  	"github.com/observiq/carbon/entry"
    16  	"github.com/observiq/carbon/operator"
    17  	"github.com/observiq/carbon/testutil"
    18  	"github.com/stretchr/testify/require"
    19  	"google.golang.org/api/option"
    20  	"google.golang.org/genproto/googleapis/api/monitoredres"
    21  	sev "google.golang.org/genproto/googleapis/logging/type"
    22  	logpb "google.golang.org/genproto/googleapis/logging/v2"
    23  	"google.golang.org/grpc"
    24  )
    25  
    26  type googleCloudTestCase struct {
    27  	name           string
    28  	config         *GoogleCloudOutputConfig
    29  	input          *entry.Entry
    30  	expectedOutput *logpb.WriteLogEntriesRequest
    31  }
    32  
    33  func googleCloudBasicConfig() *GoogleCloudOutputConfig {
    34  	cfg := NewGoogleCloudOutputConfig("test_id")
    35  	cfg.ProjectID = "test_project_id"
    36  	cfg.BufferConfig.DelayThreshold = operator.Duration{Duration: time.Millisecond}
    37  	return cfg
    38  }
    39  
    40  func googleCloudBasicWriteEntriesRequest() *logpb.WriteLogEntriesRequest {
    41  	return &logpb.WriteLogEntriesRequest{
    42  		LogName: "projects/test_project_id/logs/default",
    43  		Resource: &monitoredres.MonitoredResource{
    44  			Type: "global",
    45  			Labels: map[string]string{
    46  				"project_id": "test_project_id",
    47  			},
    48  		},
    49  	}
    50  }
    51  
    52  func googleCloudTimes() (time.Time, *tspb.Timestamp) {
    53  	now, _ := time.Parse(time.RFC3339, time.RFC3339)
    54  	protoTs, _ := ptypes.TimestampProto(now)
    55  	return now, protoTs
    56  }
    57  
    58  func TestGoogleCloudOutput(t *testing.T) {
    59  
    60  	now, protoTs := googleCloudTimes()
    61  
    62  	cases := []googleCloudTestCase{
    63  		{
    64  			"Basic",
    65  			googleCloudBasicConfig(),
    66  			&entry.Entry{
    67  				Timestamp: now,
    68  				Record: map[string]interface{}{
    69  					"message": "test message",
    70  				},
    71  			},
    72  			func() *logpb.WriteLogEntriesRequest {
    73  				req := googleCloudBasicWriteEntriesRequest()
    74  				req.Entries = []*logpb.LogEntry{
    75  					{
    76  						Timestamp: protoTs,
    77  						Payload: &logpb.LogEntry_JsonPayload{JsonPayload: jsonMapToProtoStruct(map[string]interface{}{
    78  							"message": "test message",
    79  						})},
    80  					},
    81  				}
    82  				return req
    83  			}(),
    84  		},
    85  		{
    86  			"LogNameField",
    87  			func() *GoogleCloudOutputConfig {
    88  				c := googleCloudBasicConfig()
    89  				f := entry.NewRecordField("log_name")
    90  				c.LogNameField = &f
    91  				return c
    92  			}(),
    93  			&entry.Entry{
    94  				Timestamp: now,
    95  				Record: map[string]interface{}{
    96  					"message":  "test message",
    97  					"log_name": "mylogname",
    98  				},
    99  			},
   100  			func() *logpb.WriteLogEntriesRequest {
   101  				req := googleCloudBasicWriteEntriesRequest()
   102  				req.Entries = []*logpb.LogEntry{
   103  					{
   104  						LogName:   "projects/test_project_id/logs/mylogname",
   105  						Timestamp: protoTs,
   106  						Payload: &logpb.LogEntry_JsonPayload{JsonPayload: jsonMapToProtoStruct(map[string]interface{}{
   107  							"message": "test message",
   108  						})},
   109  					},
   110  				}
   111  				return req
   112  			}(),
   113  		},
   114  		{
   115  			"Labels",
   116  			func() *GoogleCloudOutputConfig {
   117  				return googleCloudBasicConfig()
   118  			}(),
   119  			&entry.Entry{
   120  				Timestamp: now,
   121  				Labels: map[string]string{
   122  					"label1": "value1",
   123  				},
   124  				Record: map[string]interface{}{
   125  					"message": "test message",
   126  				},
   127  			},
   128  			func() *logpb.WriteLogEntriesRequest {
   129  				req := googleCloudBasicWriteEntriesRequest()
   130  				req.Entries = []*logpb.LogEntry{
   131  					{
   132  						Labels: map[string]string{
   133  							"label1": "value1",
   134  						},
   135  						Timestamp: protoTs,
   136  						Payload: &logpb.LogEntry_JsonPayload{JsonPayload: jsonMapToProtoStruct(map[string]interface{}{
   137  							"message": "test message",
   138  						})},
   139  					},
   140  				}
   141  				return req
   142  			}(),
   143  		},
   144  		googleCloudSeverityTestCase(entry.Catastrophe, sev.LogSeverity_EMERGENCY),
   145  		googleCloudSeverityTestCase(entry.Severity(95), sev.LogSeverity_EMERGENCY),
   146  		googleCloudSeverityTestCase(entry.Emergency, sev.LogSeverity_EMERGENCY),
   147  		googleCloudSeverityTestCase(entry.Severity(85), sev.LogSeverity_ALERT),
   148  		googleCloudSeverityTestCase(entry.Alert, sev.LogSeverity_ALERT),
   149  		googleCloudSeverityTestCase(entry.Severity(75), sev.LogSeverity_CRITICAL),
   150  		googleCloudSeverityTestCase(entry.Critical, sev.LogSeverity_CRITICAL),
   151  		googleCloudSeverityTestCase(entry.Severity(65), sev.LogSeverity_ERROR),
   152  		googleCloudSeverityTestCase(entry.Error, sev.LogSeverity_ERROR),
   153  		googleCloudSeverityTestCase(entry.Severity(55), sev.LogSeverity_WARNING),
   154  		googleCloudSeverityTestCase(entry.Warning, sev.LogSeverity_WARNING),
   155  		googleCloudSeverityTestCase(entry.Severity(45), sev.LogSeverity_NOTICE),
   156  		googleCloudSeverityTestCase(entry.Notice, sev.LogSeverity_NOTICE),
   157  		googleCloudSeverityTestCase(entry.Severity(35), sev.LogSeverity_INFO),
   158  		googleCloudSeverityTestCase(entry.Info, sev.LogSeverity_INFO),
   159  		googleCloudSeverityTestCase(entry.Severity(25), sev.LogSeverity_DEBUG),
   160  		googleCloudSeverityTestCase(entry.Debug, sev.LogSeverity_DEBUG),
   161  		googleCloudSeverityTestCase(entry.Severity(15), sev.LogSeverity_DEBUG),
   162  		googleCloudSeverityTestCase(entry.Trace, sev.LogSeverity_DEBUG),
   163  		googleCloudSeverityTestCase(entry.Severity(5), sev.LogSeverity_DEBUG),
   164  		googleCloudSeverityTestCase(entry.Default, sev.LogSeverity_DEFAULT),
   165  		{
   166  			"TraceAndSpanFields",
   167  			func() *GoogleCloudOutputConfig {
   168  				c := googleCloudBasicConfig()
   169  				traceField := entry.NewRecordField("trace")
   170  				spanIDField := entry.NewRecordField("span_id")
   171  				c.TraceField = &traceField
   172  				c.SpanIDField = &spanIDField
   173  				return c
   174  			}(),
   175  			&entry.Entry{
   176  				Timestamp: now,
   177  				Record: map[string]interface{}{
   178  					"message": "test message",
   179  					"trace":   "projects/my-projectid/traces/06796866738c859f2f19b7cfb3214824",
   180  					"span_id": "000000000000004a",
   181  				},
   182  			},
   183  			func() *logpb.WriteLogEntriesRequest {
   184  				req := googleCloudBasicWriteEntriesRequest()
   185  				req.Entries = []*logpb.LogEntry{
   186  					{
   187  						Trace:     "projects/my-projectid/traces/06796866738c859f2f19b7cfb3214824",
   188  						SpanId:    "000000000000004a",
   189  						Timestamp: protoTs,
   190  						Payload: &logpb.LogEntry_JsonPayload{JsonPayload: jsonMapToProtoStruct(map[string]interface{}{
   191  							"message": "test message",
   192  						})},
   193  					},
   194  				}
   195  				return req
   196  			}(),
   197  		},
   198  	}
   199  
   200  	for _, tc := range cases {
   201  		t.Run(tc.name, func(t *testing.T) {
   202  			buildContext := testutil.NewBuildContext(t)
   203  			cloudOutput, err := tc.config.Build(buildContext)
   204  			require.NoError(t, err)
   205  
   206  			conn, received, stop, err := startServer()
   207  			require.NoError(t, err)
   208  			defer stop()
   209  
   210  			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   211  			defer cancel()
   212  
   213  			client, err := vkit.NewClient(ctx, option.WithGRPCConn(conn))
   214  			require.NoError(t, err)
   215  
   216  			cloudOutput.(*GoogleCloudOutput).client = client
   217  
   218  			err = cloudOutput.Process(context.Background(), tc.input)
   219  			require.NoError(t, err)
   220  
   221  			select {
   222  			case req := <-received:
   223  				require.Equal(t, tc.expectedOutput, req)
   224  			case <-time.After(time.Second):
   225  				require.FailNow(t, "Timed out waiting for writeLogEntries request")
   226  			}
   227  		})
   228  	}
   229  }
   230  
   231  func googleCloudSeverityTestCase(s entry.Severity, expected sev.LogSeverity) googleCloudTestCase {
   232  	now, protoTs := googleCloudTimes()
   233  	return googleCloudTestCase{
   234  		fmt.Sprintf("Severity%s", s),
   235  		func() *GoogleCloudOutputConfig {
   236  			return googleCloudBasicConfig()
   237  		}(),
   238  		&entry.Entry{
   239  			Timestamp: now,
   240  			Severity:  s,
   241  			Record: map[string]interface{}{
   242  				"message": "test message",
   243  			},
   244  		},
   245  		func() *logpb.WriteLogEntriesRequest {
   246  			req := googleCloudBasicWriteEntriesRequest()
   247  			req.Entries = []*logpb.LogEntry{
   248  				{
   249  					Severity:  expected,
   250  					Timestamp: protoTs,
   251  					Payload: &logpb.LogEntry_JsonPayload{JsonPayload: jsonMapToProtoStruct(map[string]interface{}{
   252  						"message": "test message",
   253  					})},
   254  				},
   255  			}
   256  			return req
   257  		}(),
   258  	}
   259  }
   260  
   261  type googleCloudProtobufTest struct {
   262  	name   string
   263  	record interface{}
   264  }
   265  
   266  func (g *googleCloudProtobufTest) Run(t *testing.T) {
   267  	t.Run(g.name, func(t *testing.T) {
   268  		e := &logpb.LogEntry{}
   269  		err := setPayload(e, g.record)
   270  		require.NoError(t, err)
   271  	})
   272  }
   273  
   274  func TestGoogleCloudSetPayload(t *testing.T) {
   275  	cases := []googleCloudProtobufTest{
   276  		{
   277  			"string",
   278  			"test",
   279  		},
   280  		{
   281  			"[]byte",
   282  			[]byte("test"),
   283  		},
   284  		{
   285  			"map[string]string",
   286  			map[string]string{"test": "val"},
   287  		},
   288  		{
   289  			"Nested_[]string",
   290  			map[string]interface{}{
   291  				"sub": []string{"1", "2"},
   292  			},
   293  		},
   294  		{
   295  			"Nested_[]int",
   296  			map[string]interface{}{
   297  				"sub": []int{1, 2},
   298  			},
   299  		},
   300  		{
   301  			"Nested_uint32",
   302  			map[string]interface{}{
   303  				"sub": uint32(32),
   304  			},
   305  		},
   306  		{
   307  			"DeepNested_map",
   308  			map[string]interface{}{
   309  				"0": map[string]map[string]map[string]string{
   310  					"1": {"2": {"3": "test"}},
   311  				},
   312  			},
   313  		},
   314  		{
   315  			"DeepNested_slice",
   316  			map[string]interface{}{
   317  				"0": [][][]string{{{"0", "1"}}},
   318  			},
   319  		},
   320  		{
   321  			"AnonymousStruct",
   322  			map[string]interface{}{
   323  				"0": struct{ Field string }{Field: "test"},
   324  			},
   325  		},
   326  		{
   327  			"NamedStruct",
   328  			map[string]interface{}{
   329  				"0": time.Now(),
   330  			},
   331  		},
   332  	}
   333  
   334  	for _, tc := range cases {
   335  		tc.Run(t)
   336  	}
   337  }
   338  
   339  // Adapted from https://github.com/googleapis/google-cloud-go/blob/master/internal/testutil/server.go
   340  type loggingHandler struct {
   341  	logpb.LoggingServiceV2Server
   342  
   343  	received chan *logpb.WriteLogEntriesRequest
   344  }
   345  
   346  func (h *loggingHandler) WriteLogEntries(_ context.Context, req *logpb.WriteLogEntriesRequest) (*logpb.WriteLogEntriesResponse, error) {
   347  	h.received <- req
   348  	return &logpb.WriteLogEntriesResponse{}, nil
   349  }
   350  
   351  func startServer() (*grpc.ClientConn, chan *logpb.WriteLogEntriesRequest, func(), error) {
   352  	received := make(chan *logpb.WriteLogEntriesRequest, 10)
   353  	serv := grpc.NewServer()
   354  	logpb.RegisterLoggingServiceV2Server(serv, &loggingHandler{
   355  		received: received,
   356  	})
   357  
   358  	lis, err := net.Listen("tcp", "localhost:0")
   359  	if err != nil {
   360  		return nil, nil, nil, err
   361  	}
   362  	go serv.Serve(lis)
   363  
   364  	conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
   365  	if err != nil {
   366  		return nil, nil, nil, err
   367  	}
   368  
   369  	return conn, received, serv.Stop, nil
   370  }
   371  
   372  type googleCloudOutputBenchmark struct {
   373  	name      string
   374  	entry     *entry.Entry
   375  	configMod func(*GoogleCloudOutputConfig)
   376  }
   377  
   378  func (g *googleCloudOutputBenchmark) Run(b *testing.B) {
   379  	conn, received, stop, err := startServer()
   380  	require.NoError(b, err)
   381  	defer stop()
   382  
   383  	client, err := vkit.NewClient(context.Background(), option.WithGRPCConn(conn))
   384  	require.NoError(b, err)
   385  
   386  	cfg := NewGoogleCloudOutputConfig(g.name)
   387  	cfg.ProjectID = "test_project_id"
   388  	if g.configMod != nil {
   389  		g.configMod(cfg)
   390  	}
   391  	op, err := cfg.Build(testutil.NewBuildContext(b))
   392  	require.NoError(b, err)
   393  	op.(*GoogleCloudOutput).client = client
   394  	op.(*GoogleCloudOutput).timeout = 30 * time.Second
   395  
   396  	b.ResetTimer()
   397  	var wg sync.WaitGroup
   398  	wg.Add(1)
   399  	go func() {
   400  		defer wg.Done()
   401  		for i := 0; i < b.N; i++ {
   402  			op.Process(context.Background(), g.entry)
   403  		}
   404  		err = op.Stop()
   405  		require.NoError(b, err)
   406  	}()
   407  
   408  	wg.Add(1)
   409  	go func() {
   410  		defer wg.Done()
   411  		i := 0
   412  		for i < b.N {
   413  			req := <-received
   414  			i += len(req.Entries)
   415  		}
   416  	}()
   417  
   418  	wg.Wait()
   419  }
   420  
   421  func BenchmarkGoogleCloudOutput(b *testing.B) {
   422  	t := time.Date(2007, 01, 01, 10, 15, 32, 0, time.UTC)
   423  	cases := []googleCloudOutputBenchmark{
   424  		{
   425  			"Simple",
   426  			&entry.Entry{
   427  				Timestamp: t,
   428  				Record:    "test",
   429  			},
   430  			nil,
   431  		},
   432  		{
   433  			"MapRecord",
   434  			&entry.Entry{
   435  				Timestamp: t,
   436  				Record:    mapOfSize(1, 0),
   437  			},
   438  			nil,
   439  		},
   440  		{
   441  			"LargeMapRecord",
   442  			&entry.Entry{
   443  				Timestamp: t,
   444  				Record:    mapOfSize(30, 0),
   445  			},
   446  			nil,
   447  		},
   448  		{
   449  			"DeepMapRecord",
   450  			&entry.Entry{
   451  				Timestamp: t,
   452  				Record:    mapOfSize(1, 10),
   453  			},
   454  			nil,
   455  		},
   456  		{
   457  			"Labels",
   458  			&entry.Entry{
   459  				Timestamp: t,
   460  				Record:    "test",
   461  				Labels: map[string]string{
   462  					"test": "val",
   463  				},
   464  			},
   465  			nil,
   466  		},
   467  		{
   468  			"NoCompression",
   469  			&entry.Entry{
   470  				Timestamp: t,
   471  				Record:    "test",
   472  			},
   473  			func(cfg *GoogleCloudOutputConfig) {
   474  				cfg.UseCompression = false
   475  			},
   476  		},
   477  	}
   478  
   479  	for _, tc := range cases {
   480  		b.Run(tc.name, tc.Run)
   481  	}
   482  }
   483  
   484  func mapOfSize(keys, depth int) map[string]interface{} {
   485  	m := make(map[string]interface{})
   486  	for i := 0; i < keys; i++ {
   487  		if depth == 0 {
   488  			m["k"+strconv.Itoa(i)] = "v" + strconv.Itoa(i)
   489  		} else {
   490  			m["k"+strconv.Itoa(i)] = mapOfSize(keys, depth-1)
   491  		}
   492  	}
   493  	return m
   494  }