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 }