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

     1  package output
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"strconv"
     8  
     9  	elasticsearch "github.com/elastic/go-elasticsearch/v7"
    10  	"github.com/elastic/go-elasticsearch/v7/esapi"
    11  	uuid "github.com/hashicorp/go-uuid"
    12  	"github.com/observiq/carbon/entry"
    13  	"github.com/observiq/carbon/errors"
    14  	"github.com/observiq/carbon/operator"
    15  	"github.com/observiq/carbon/operator/buffer"
    16  	"github.com/observiq/carbon/operator/helper"
    17  	"go.uber.org/zap"
    18  )
    19  
    20  func init() {
    21  	operator.Register("elastic_output", func() operator.Builder { return NewElasticOutputConfig("") })
    22  }
    23  
    24  func NewElasticOutputConfig(operatorID string) *ElasticOutputConfig {
    25  	return &ElasticOutputConfig{
    26  		OutputConfig: helper.NewOutputConfig(operatorID, "elastic_output"),
    27  		BufferConfig: buffer.NewConfig(),
    28  	}
    29  }
    30  
    31  // ElasticOutputConfig is the configuration of an elasticsearch output operator.
    32  type ElasticOutputConfig struct {
    33  	helper.OutputConfig `yaml:",inline"`
    34  	BufferConfig        buffer.Config `json:"buffer" yaml:"buffer"`
    35  
    36  	Addresses  []string     `json:"addresses"             yaml:"addresses,flow"`
    37  	Username   string       `json:"username"              yaml:"username"`
    38  	Password   string       `json:"password"              yaml:"password"`
    39  	CloudID    string       `json:"cloud_id"              yaml:"cloud_id"`
    40  	APIKey     string       `json:"api_key"               yaml:"api_key"`
    41  	IndexField *entry.Field `json:"index_field,omitempty" yaml:"index_field,omitempty"`
    42  	IDField    *entry.Field `json:"id_field,omitempty"    yaml:"id_field,omitempty"`
    43  }
    44  
    45  // Build will build an elasticsearch output operator.
    46  func (c ElasticOutputConfig) Build(context operator.BuildContext) (operator.Operator, error) {
    47  	outputOperator, err := c.OutputConfig.Build(context)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	cfg := elasticsearch.Config{
    53  		Addresses: c.Addresses,
    54  		Username:  c.Username,
    55  		Password:  c.Password,
    56  		CloudID:   c.CloudID,
    57  		APIKey:    c.APIKey,
    58  	}
    59  
    60  	client, err := elasticsearch.NewClient(cfg)
    61  	if err != nil {
    62  		return nil, errors.NewError(
    63  			"The Elasticsearch client failed to initialize.",
    64  			"Review the underlying error message to troubleshoot the issue.",
    65  			"underlying_error", err.Error(),
    66  		)
    67  	}
    68  
    69  	buffer, err := c.BufferConfig.Build()
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	elasticOutput := &ElasticOutput{
    75  		OutputOperator: outputOperator,
    76  		Buffer:         buffer,
    77  		client:         client,
    78  		indexField:     c.IndexField,
    79  		idField:        c.IDField,
    80  	}
    81  
    82  	buffer.SetHandler(elasticOutput)
    83  
    84  	return elasticOutput, nil
    85  }
    86  
    87  // ElasticOutput is an operator that sends entries to elasticsearch.
    88  type ElasticOutput struct {
    89  	helper.OutputOperator
    90  	buffer.Buffer
    91  
    92  	client     *elasticsearch.Client
    93  	indexField *entry.Field
    94  	idField    *entry.Field
    95  }
    96  
    97  // ProcessMulti will send entries to elasticsearch.
    98  func (e *ElasticOutput) ProcessMulti(ctx context.Context, entries []*entry.Entry) error {
    99  	type indexDirective struct {
   100  		Index struct {
   101  			Index string `json:"_index"`
   102  			ID    string `json:"_id"`
   103  		} `json:"index"`
   104  	}
   105  
   106  	// The bulk API expects newline-delimited json strings, with an operation directive
   107  	// immediately followed by the document.
   108  	// https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-bulk.html
   109  	var buffer bytes.Buffer
   110  	var err error
   111  	for _, entry := range entries {
   112  		directive := indexDirective{}
   113  		directive.Index.Index, err = e.FindIndex(entry)
   114  		if err != nil {
   115  			e.Warnw("Failed to find index", zap.Any("error", err))
   116  			continue
   117  		}
   118  
   119  		directive.Index.ID, err = e.FindID(entry)
   120  		if err != nil {
   121  			e.Warnw("Failed to find id", zap.Any("error", err))
   122  			continue
   123  		}
   124  
   125  		directiveJSON, err := json.Marshal(directive)
   126  		if err != nil {
   127  			e.Warnw("Failed to marshal directive JSON", zap.Any("error", err))
   128  			continue
   129  		}
   130  
   131  		entryJSON, err := json.Marshal(entry)
   132  		if err != nil {
   133  			e.Warnw("Failed to marshal entry JSON", zap.Any("error", err))
   134  			continue
   135  		}
   136  
   137  		buffer.Write(directiveJSON)
   138  		buffer.Write([]byte("\n"))
   139  		buffer.Write(entryJSON)
   140  		buffer.Write([]byte("\n"))
   141  	}
   142  
   143  	request := esapi.BulkRequest{
   144  		Body: bytes.NewReader(buffer.Bytes()),
   145  	}
   146  
   147  	res, err := request.Do(ctx, e.client)
   148  	if err != nil {
   149  		return errors.NewError(
   150  			"Client failed to submit request to elasticsearch.",
   151  			"Review the underlying error message to troubleshoot the issue",
   152  			"underlying_error", err.Error(),
   153  		)
   154  	}
   155  
   156  	defer res.Body.Close()
   157  
   158  	if res.IsError() {
   159  		return errors.NewError(
   160  			"Request to elasticsearch returned a failure code.",
   161  			"Review status and status code for further details.",
   162  			"status_code", strconv.Itoa(res.StatusCode),
   163  			"status", res.Status(),
   164  		)
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  // FindIndex will find an index that will represent an entry in elasticsearch.
   171  func (e *ElasticOutput) FindIndex(entry *entry.Entry) (string, error) {
   172  	if e.indexField == nil {
   173  		return "default", nil
   174  	}
   175  
   176  	var value string
   177  	err := entry.Read(*e.indexField, &value)
   178  	if err != nil {
   179  		return "", errors.Wrap(err, "extract index from record")
   180  	}
   181  
   182  	return value, nil
   183  }
   184  
   185  // FindID will find the id that will represent an entry in elasticsearch.
   186  func (e *ElasticOutput) FindID(entry *entry.Entry) (string, error) {
   187  	if e.idField == nil {
   188  		return uuid.GenerateUUID()
   189  	}
   190  
   191  	var value string
   192  	err := entry.Read(*e.idField, &value)
   193  	if err != nil {
   194  		return "", errors.Wrap(err, "extract id from record")
   195  	}
   196  
   197  	return value, nil
   198  }