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 }