github.com/observiq/carbon@v0.9.11-0.20200820160507-1b872e368a5e/operator/builtin/output/google_cloud.go (about) 1 package output 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "net/url" 8 "reflect" 9 "time" 10 11 vkit "cloud.google.com/go/logging/apiv2" 12 "github.com/golang/protobuf/ptypes" 13 structpb "github.com/golang/protobuf/ptypes/struct" 14 "github.com/observiq/carbon/entry" 15 "github.com/observiq/carbon/errors" 16 "github.com/observiq/carbon/internal/version" 17 "github.com/observiq/carbon/operator" 18 "github.com/observiq/carbon/operator/buffer" 19 "github.com/observiq/carbon/operator/helper" 20 "go.uber.org/zap" 21 "golang.org/x/oauth2/google" 22 "google.golang.org/api/option" 23 mrpb "google.golang.org/genproto/googleapis/api/monitoredres" 24 sev "google.golang.org/genproto/googleapis/logging/type" 25 logpb "google.golang.org/genproto/googleapis/logging/v2" 26 "google.golang.org/grpc" 27 "google.golang.org/grpc/encoding/gzip" 28 ) 29 30 func init() { 31 operator.Register("google_cloud_output", func() operator.Builder { return NewGoogleCloudOutputConfig("") }) 32 } 33 34 func NewGoogleCloudOutputConfig(operatorID string) *GoogleCloudOutputConfig { 35 return &GoogleCloudOutputConfig{ 36 OutputConfig: helper.NewOutputConfig(operatorID, "google_cloud_output"), 37 BufferConfig: buffer.NewConfig(), 38 Timeout: operator.Duration{Duration: 30 * time.Second}, 39 UseCompression: true, 40 } 41 } 42 43 // GoogleCloudOutputConfig is the configuration of a google cloud output operator. 44 type GoogleCloudOutputConfig struct { 45 helper.OutputConfig `yaml:",inline"` 46 BufferConfig buffer.Config `json:"buffer,omitempty" yaml:"buffer,omitempty"` 47 48 Credentials string `json:"credentials,omitempty" yaml:"credentials,omitempty"` 49 CredentialsFile string `json:"credentials_file,omitempty" yaml:"credentials_file,omitempty"` 50 ProjectID string `json:"project_id" yaml:"project_id"` 51 LogNameField *entry.Field `json:"log_name_field,omitempty" yaml:"log_name_field,omitempty"` 52 TraceField *entry.Field `json:"trace_field,omitempty" yaml:"trace_field,omitempty"` 53 SpanIDField *entry.Field `json:"span_id_field,omitempty" yaml:"span_id_field,omitempty"` 54 Timeout operator.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` 55 UseCompression bool `json:"use_compression,omitempty" yaml:"use_compression,omitempty"` 56 } 57 58 // Build will build a google cloud output operator. 59 func (c GoogleCloudOutputConfig) Build(buildContext operator.BuildContext) (operator.Operator, error) { 60 outputOperator, err := c.OutputConfig.Build(buildContext) 61 if err != nil { 62 return nil, err 63 } 64 65 newBuffer, err := c.BufferConfig.Build() 66 if err != nil { 67 return nil, err 68 } 69 70 googleCloudOutput := &GoogleCloudOutput{ 71 OutputOperator: outputOperator, 72 credentials: c.Credentials, 73 credentialsFile: c.CredentialsFile, 74 projectID: c.ProjectID, 75 Buffer: newBuffer, 76 logNameField: c.LogNameField, 77 traceField: c.TraceField, 78 spanIDField: c.SpanIDField, 79 timeout: c.Timeout.Raw(), 80 useCompression: c.UseCompression, 81 } 82 83 newBuffer.SetHandler(googleCloudOutput) 84 85 return googleCloudOutput, nil 86 } 87 88 // GoogleCloudOutput is an operator that sends logs to google cloud logging. 89 type GoogleCloudOutput struct { 90 helper.OutputOperator 91 buffer.Buffer 92 93 credentials string 94 credentialsFile string 95 projectID string 96 97 logNameField *entry.Field 98 traceField *entry.Field 99 spanIDField *entry.Field 100 useCompression bool 101 102 client *vkit.Client 103 timeout time.Duration 104 } 105 106 // Start will start the google cloud logger. 107 func (p *GoogleCloudOutput) Start() error { 108 var credentials *google.Credentials 109 var err error 110 scope := "https://www.googleapis.com/auth/logging.write" 111 switch { 112 case p.credentials != "" && p.credentialsFile != "": 113 return errors.NewError("at most one of credentials or credentials_file can be configured", "") 114 case p.credentials != "": 115 credentials, err = google.CredentialsFromJSON(context.Background(), []byte(p.credentials), scope) 116 if err != nil { 117 return fmt.Errorf("parse credentials: %s", err) 118 } 119 case p.credentialsFile != "": 120 credentialsBytes, err := ioutil.ReadFile(p.credentialsFile) 121 if err != nil { 122 return fmt.Errorf("read credentials file: %s", err) 123 } 124 credentials, err = google.CredentialsFromJSON(context.Background(), credentialsBytes, scope) 125 if err != nil { 126 return fmt.Errorf("parse credentials: %s", err) 127 } 128 default: 129 credentials, err = google.FindDefaultCredentials(context.Background(), scope) 130 if err != nil { 131 return fmt.Errorf("get default credentials: %s", err) 132 } 133 } 134 135 if p.projectID == "" && credentials.ProjectID == "" { 136 return fmt.Errorf("no project id found on google creds") 137 } 138 139 if p.projectID == "" { 140 p.projectID = credentials.ProjectID 141 } 142 143 options := make([]option.ClientOption, 0, 2) 144 options = append(options, option.WithCredentials(credentials)) 145 options = append(options, option.WithUserAgent("CarbonLogAgent/"+version.GetVersion())) 146 if p.useCompression { 147 options = append(options, option.WithGRPCDialOption(grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)))) 148 } 149 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 150 defer cancel() 151 152 client, err := vkit.NewClient(ctx, options...) 153 if err != nil { 154 return fmt.Errorf("create client: %w", err) 155 } 156 p.client = client 157 158 // Test writing a log message 159 ctx, cancel = context.WithTimeout(context.Background(), p.timeout) 160 defer cancel() 161 testEntry := entry.New() 162 testEntry.Record = map[string]interface{}{"message": "Test connection"} 163 err = p.ProcessMulti(ctx, []*entry.Entry{testEntry}) 164 if err != nil { 165 return fmt.Errorf("test connection: %s", err) 166 } 167 168 return nil 169 } 170 171 // Stop will flush the google cloud logger and close the underlying connection 172 func (p *GoogleCloudOutput) Stop() error { 173 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 174 defer cancel() 175 err := p.Buffer.Flush(ctx) 176 if err != nil { 177 p.Warnw("Failed to flush", zap.Error(err)) 178 } 179 return p.client.Close() 180 } 181 182 // ProcessMulti will process multiple log entries and send them in batch to google cloud logging. 183 func (p *GoogleCloudOutput) ProcessMulti(ctx context.Context, entries []*entry.Entry) error { 184 pbEntries := make([]*logpb.LogEntry, 0, len(entries)) 185 for _, entry := range entries { 186 pbEntry, err := p.createProtobufEntry(entry) 187 if err != nil { 188 p.Errorw("Failed to create protobuf entry. Dropping entry", zap.Any("error", err)) 189 continue 190 } 191 pbEntries = append(pbEntries, pbEntry) 192 } 193 194 req := logpb.WriteLogEntriesRequest{ 195 LogName: p.toLogNamePath("default"), 196 Entries: pbEntries, 197 Resource: globalResource(p.projectID), 198 } 199 200 ctx, cancel := context.WithTimeout(ctx, p.timeout) 201 defer cancel() 202 _, err := p.client.WriteLogEntries(ctx, &req) 203 if err != nil { 204 return fmt.Errorf("write log entries: %s", err) 205 } 206 207 return nil 208 } 209 210 func (p *GoogleCloudOutput) toLogNamePath(logName string) string { 211 return fmt.Sprintf("projects/%s/logs/%s", p.projectID, url.PathEscape(logName)) 212 } 213 214 func (p *GoogleCloudOutput) createProtobufEntry(e *entry.Entry) (newEntry *logpb.LogEntry, err error) { 215 ts, err := ptypes.TimestampProto(e.Timestamp) 216 if err != nil { 217 return nil, err 218 } 219 220 newEntry = &logpb.LogEntry{ 221 Timestamp: ts, 222 Labels: e.Labels, 223 } 224 225 if p.logNameField != nil { 226 var rawLogName string 227 err := e.Read(*p.logNameField, &rawLogName) 228 if err != nil { 229 p.Warnw("Failed to set log name", zap.Error(err), "entry", e) 230 } else { 231 newEntry.LogName = p.toLogNamePath(rawLogName) 232 e.Delete(*p.logNameField) 233 } 234 } 235 236 if p.traceField != nil { 237 err := e.Read(*p.traceField, &newEntry.Trace) 238 if err != nil { 239 p.Warnw("Failed to set trace", zap.Error(err), "entry", e) 240 } else { 241 e.Delete(*p.traceField) 242 } 243 } 244 245 if p.spanIDField != nil { 246 err := e.Read(*p.spanIDField, &newEntry.SpanId) 247 if err != nil { 248 p.Warnw("Failed to set span ID", zap.Error(err), "entry", e) 249 } else { 250 e.Delete(*p.spanIDField) 251 } 252 } 253 254 newEntry.Severity = interpretSeverity(e.Severity) 255 err = setPayload(newEntry, e.Record) 256 if err != nil { 257 return nil, errors.Wrap(err, "set entry payload") 258 } 259 260 return newEntry, nil 261 } 262 263 func setPayload(entry *logpb.LogEntry, record interface{}) (err error) { 264 // Protect against the panic condition inside `jsonValueToStructValue` 265 defer func() { 266 if r := recover(); r != nil { 267 err = fmt.Errorf(r.(string)) 268 } 269 }() 270 switch p := record.(type) { 271 case string: 272 entry.Payload = &logpb.LogEntry_TextPayload{TextPayload: p} 273 case []byte: 274 entry.Payload = &logpb.LogEntry_TextPayload{TextPayload: string(p)} 275 case map[string]interface{}: 276 s := jsonMapToProtoStruct(p) 277 entry.Payload = &logpb.LogEntry_JsonPayload{JsonPayload: s} 278 case map[string]string: 279 fields := map[string]*structpb.Value{} 280 for k, v := range p { 281 fields[k] = jsonValueToStructValue(v) 282 } 283 entry.Payload = &logpb.LogEntry_JsonPayload{JsonPayload: &structpb.Struct{Fields: fields}} 284 default: 285 return fmt.Errorf("cannot convert record of type %T to a protobuf representation", record) 286 } 287 288 return nil 289 } 290 291 func jsonMapToProtoStruct(m map[string]interface{}) *structpb.Struct { 292 fields := map[string]*structpb.Value{} 293 for k, v := range m { 294 fields[k] = jsonValueToStructValue(v) 295 } 296 return &structpb.Struct{Fields: fields} 297 } 298 299 func jsonValueToStructValue(v interface{}) *structpb.Value { 300 switch x := v.(type) { 301 case bool: 302 return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: x}} 303 case float32: 304 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 305 case float64: 306 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: x}} 307 case int: 308 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 309 case int8: 310 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 311 case int16: 312 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 313 case int32: 314 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 315 case int64: 316 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 317 case uint: 318 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 319 case uint8: 320 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 321 case uint16: 322 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 323 case uint32: 324 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 325 case uint64: 326 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(x)}} 327 case string: 328 return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: x}} 329 case nil: 330 return &structpb.Value{Kind: &structpb.Value_NullValue{}} 331 case map[string]interface{}: 332 return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: jsonMapToProtoStruct(x)}} 333 case map[string]map[string]string: 334 fields := map[string]*structpb.Value{} 335 for k, v := range x { 336 fields[k] = jsonValueToStructValue(v) 337 } 338 return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: fields}}} 339 case map[string]string: 340 fields := map[string]*structpb.Value{} 341 for k, v := range x { 342 fields[k] = &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: v}} 343 } 344 return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: fields}}} 345 case []interface{}: 346 var vals []*structpb.Value 347 for _, e := range x { 348 vals = append(vals, jsonValueToStructValue(e)) 349 } 350 return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: vals}}} 351 case []string: 352 var vals []*structpb.Value 353 for _, e := range x { 354 vals = append(vals, jsonValueToStructValue(e)) 355 } 356 return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: vals}}} 357 default: 358 // Fallback to reflection for other types 359 return reflectToValue(reflect.ValueOf(v)) 360 } 361 } 362 363 func reflectToValue(v reflect.Value) *structpb.Value { 364 switch v.Kind() { 365 case reflect.Bool: 366 return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: v.Bool()}} 367 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 368 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v.Int())}} 369 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 370 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: float64(v.Uint())}} 371 case reflect.Float32, reflect.Float64: 372 return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: v.Float()}} 373 case reflect.Ptr: 374 if v.IsNil() { 375 return nil 376 } 377 return reflectToValue(reflect.Indirect(v)) 378 case reflect.Array, reflect.Slice: 379 size := v.Len() 380 if size == 0 { 381 return nil 382 } 383 values := make([]*structpb.Value, size) 384 for i := 0; i < size; i++ { 385 values[i] = reflectToValue(v.Index(i)) 386 } 387 return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: values}}} 388 case reflect.Struct: 389 t := v.Type() 390 size := v.NumField() 391 if size == 0 { 392 return nil 393 } 394 fields := make(map[string]*structpb.Value, size) 395 for i := 0; i < size; i++ { 396 name := t.Field(i).Name 397 // Better way? 398 if len(name) > 0 && 'A' <= name[0] && name[0] <= 'Z' { 399 fields[name] = reflectToValue(v.Field(i)) 400 } 401 } 402 if len(fields) == 0 { 403 return nil 404 } 405 return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: fields}}} 406 case reflect.Map: 407 keys := v.MapKeys() 408 if len(keys) == 0 { 409 return nil 410 } 411 fields := make(map[string]*structpb.Value, len(keys)) 412 for _, k := range keys { 413 if k.Kind() == reflect.String { 414 fields[k.String()] = reflectToValue(v.MapIndex(k)) 415 } 416 } 417 if len(fields) == 0 { 418 return nil 419 } 420 return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: fields}}} 421 default: 422 // Last resort 423 return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: fmt.Sprint(v)}} 424 } 425 } 426 427 func globalResource(projectID string) *mrpb.MonitoredResource { 428 return &mrpb.MonitoredResource{ 429 Type: "global", 430 Labels: map[string]string{ 431 "project_id": projectID, 432 }, 433 } 434 } 435 436 var fastSev = map[entry.Severity]sev.LogSeverity{ 437 entry.Catastrophe: sev.LogSeverity_EMERGENCY, 438 entry.Emergency: sev.LogSeverity_EMERGENCY, 439 entry.Alert: sev.LogSeverity_ALERT, 440 entry.Critical: sev.LogSeverity_CRITICAL, 441 entry.Error: sev.LogSeverity_ERROR, 442 entry.Warning: sev.LogSeverity_WARNING, 443 entry.Notice: sev.LogSeverity_NOTICE, 444 entry.Info: sev.LogSeverity_INFO, 445 entry.Debug: sev.LogSeverity_DEBUG, 446 entry.Trace: sev.LogSeverity_DEBUG, 447 entry.Default: sev.LogSeverity_DEFAULT, 448 } 449 450 func interpretSeverity(s entry.Severity) sev.LogSeverity { 451 if logSev, ok := fastSev[s]; ok { 452 return logSev 453 } 454 455 switch { 456 case s >= entry.Emergency: 457 return sev.LogSeverity_EMERGENCY 458 case s >= entry.Alert: 459 return sev.LogSeverity_ALERT 460 case s >= entry.Critical: 461 return sev.LogSeverity_CRITICAL 462 case s >= entry.Error: 463 return sev.LogSeverity_ERROR 464 case s >= entry.Warning: 465 return sev.LogSeverity_WARNING 466 case s >= entry.Notice: 467 return sev.LogSeverity_NOTICE 468 case s >= entry.Info: 469 return sev.LogSeverity_INFO 470 case s > entry.Default: 471 return sev.LogSeverity_DEBUG 472 default: 473 return sev.LogSeverity_DEFAULT 474 } 475 }