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  }