storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/event/target/elasticsearch.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2018 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package target 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "net/url" 24 "os" 25 "path/filepath" 26 "strings" 27 "time" 28 29 "github.com/pkg/errors" 30 31 "storj.io/minio/pkg/event" 32 xnet "storj.io/minio/pkg/net" 33 34 "github.com/olivere/elastic/v7" 35 ) 36 37 // Elastic constants 38 const ( 39 ElasticFormat = "format" 40 ElasticURL = "url" 41 ElasticIndex = "index" 42 ElasticQueueDir = "queue_dir" 43 ElasticQueueLimit = "queue_limit" 44 ElasticUsername = "username" 45 ElasticPassword = "password" 46 47 EnvElasticEnable = "MINIO_NOTIFY_ELASTICSEARCH_ENABLE" 48 EnvElasticFormat = "MINIO_NOTIFY_ELASTICSEARCH_FORMAT" 49 EnvElasticURL = "MINIO_NOTIFY_ELASTICSEARCH_URL" 50 EnvElasticIndex = "MINIO_NOTIFY_ELASTICSEARCH_INDEX" 51 EnvElasticQueueDir = "MINIO_NOTIFY_ELASTICSEARCH_QUEUE_DIR" 52 EnvElasticQueueLimit = "MINIO_NOTIFY_ELASTICSEARCH_QUEUE_LIMIT" 53 EnvElasticUsername = "MINIO_NOTIFY_ELASTICSEARCH_USERNAME" 54 EnvElasticPassword = "MINIO_NOTIFY_ELASTICSEARCH_PASSWORD" 55 ) 56 57 // ElasticsearchArgs - Elasticsearch target arguments. 58 type ElasticsearchArgs struct { 59 Enable bool `json:"enable"` 60 Format string `json:"format"` 61 URL xnet.URL `json:"url"` 62 Index string `json:"index"` 63 QueueDir string `json:"queueDir"` 64 QueueLimit uint64 `json:"queueLimit"` 65 Transport *http.Transport `json:"-"` 66 Username string `json:"username"` 67 Password string `json:"password"` 68 } 69 70 // Validate ElasticsearchArgs fields 71 func (a ElasticsearchArgs) Validate() error { 72 if !a.Enable { 73 return nil 74 } 75 if a.URL.IsEmpty() { 76 return errors.New("empty URL") 77 } 78 if a.Format != "" { 79 f := strings.ToLower(a.Format) 80 if f != event.NamespaceFormat && f != event.AccessFormat { 81 return errors.New("format value unrecognized") 82 } 83 } 84 if a.Index == "" { 85 return errors.New("empty index value") 86 } 87 88 if (a.Username == "" && a.Password != "") || (a.Username != "" && a.Password == "") { 89 return errors.New("username and password should be set in pairs") 90 } 91 92 return nil 93 } 94 95 // ElasticsearchTarget - Elasticsearch target. 96 type ElasticsearchTarget struct { 97 id event.TargetID 98 args ElasticsearchArgs 99 client *elastic.Client 100 store Store 101 loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}) 102 } 103 104 // ID - returns target ID. 105 func (target *ElasticsearchTarget) ID() event.TargetID { 106 return target.id 107 } 108 109 // HasQueueStore - Checks if the queueStore has been configured for the target 110 func (target *ElasticsearchTarget) HasQueueStore() bool { 111 return target.store != nil 112 } 113 114 // IsActive - Return true if target is up and active 115 func (target *ElasticsearchTarget) IsActive() (bool, error) { 116 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 117 defer cancel() 118 119 if target.client == nil { 120 client, err := newClient(target.args) 121 if err != nil { 122 return false, err 123 } 124 target.client = client 125 } 126 _, code, err := target.client.Ping(target.args.URL.String()).HttpHeadOnly(true).Do(ctx) 127 if err != nil { 128 if elastic.IsConnErr(err) || elastic.IsContextErr(err) || xnet.IsNetworkOrHostDown(err, false) { 129 return false, errNotConnected 130 } 131 return false, err 132 } 133 return !(code >= http.StatusBadRequest), nil 134 } 135 136 // Save - saves the events to the store if queuestore is configured, which will be replayed when the elasticsearch connection is active. 137 func (target *ElasticsearchTarget) Save(eventData event.Event) error { 138 if target.store != nil { 139 return target.store.Put(eventData) 140 } 141 err := target.send(eventData) 142 if elastic.IsConnErr(err) || elastic.IsContextErr(err) || xnet.IsNetworkOrHostDown(err, false) { 143 return errNotConnected 144 } 145 return err 146 } 147 148 // send - sends the event to the target. 149 func (target *ElasticsearchTarget) send(eventData event.Event) error { 150 151 var key string 152 153 exists := func() (bool, error) { 154 return target.client.Exists().Index(target.args.Index).Type("event").Id(key).Do(context.Background()) 155 } 156 157 remove := func() error { 158 exists, err := exists() 159 if err == nil && exists { 160 _, err = target.client.Delete().Index(target.args.Index).Type("event").Id(key).Do(context.Background()) 161 } 162 return err 163 } 164 165 update := func() error { 166 _, err := target.client.Index().Index(target.args.Index).Type("event").BodyJson(map[string]interface{}{"Records": []event.Event{eventData}}).Id(key).Do(context.Background()) 167 return err 168 } 169 170 add := func() error { 171 _, err := target.client.Index().Index(target.args.Index).Type("event").BodyJson(map[string]interface{}{"Records": []event.Event{eventData}}).Do(context.Background()) 172 return err 173 } 174 175 if target.args.Format == event.NamespaceFormat { 176 objectName, err := url.QueryUnescape(eventData.S3.Object.Key) 177 if err != nil { 178 return err 179 } 180 181 key = eventData.S3.Bucket.Name + "/" + objectName 182 if eventData.EventName == event.ObjectRemovedDelete { 183 err = remove() 184 } else { 185 err = update() 186 } 187 return err 188 } 189 190 if target.args.Format == event.AccessFormat { 191 return add() 192 } 193 194 return nil 195 } 196 197 // Send - reads an event from store and sends it to Elasticsearch. 198 func (target *ElasticsearchTarget) Send(eventKey string) error { 199 var err error 200 if target.client == nil { 201 target.client, err = newClient(target.args) 202 if err != nil { 203 return err 204 } 205 } 206 207 eventData, eErr := target.store.Get(eventKey) 208 if eErr != nil { 209 // The last event key in a successful batch will be sent in the channel atmost once by the replayEvents() 210 // Such events will not exist and wouldve been already been sent successfully. 211 if os.IsNotExist(eErr) { 212 return nil 213 } 214 return eErr 215 } 216 217 if err := target.send(eventData); err != nil { 218 if elastic.IsConnErr(err) || elastic.IsContextErr(err) || xnet.IsNetworkOrHostDown(err, false) { 219 return errNotConnected 220 } 221 return err 222 } 223 224 // Delete the event from store. 225 return target.store.Del(eventKey) 226 } 227 228 // Close - does nothing and available for interface compatibility. 229 func (target *ElasticsearchTarget) Close() error { 230 if target.client != nil { 231 // Stops the background processes that the client is running. 232 target.client.Stop() 233 } 234 return nil 235 } 236 237 // createIndex - creates the index if it does not exist. 238 func createIndex(client *elastic.Client, args ElasticsearchArgs) error { 239 exists, err := client.IndexExists(args.Index).Do(context.Background()) 240 if err != nil { 241 return err 242 } 243 if !exists { 244 var createIndex *elastic.IndicesCreateResult 245 if createIndex, err = client.CreateIndex(args.Index).Do(context.Background()); err != nil { 246 return err 247 } 248 249 if !createIndex.Acknowledged { 250 return fmt.Errorf("index %v not created", args.Index) 251 } 252 } 253 return nil 254 } 255 256 // newClient - creates a new elastic client with args provided. 257 func newClient(args ElasticsearchArgs) (*elastic.Client, error) { 258 // Client options 259 options := []elastic.ClientOptionFunc{elastic.SetURL(args.URL.String()), 260 elastic.SetMaxRetries(10), 261 elastic.SetSniff(false), 262 elastic.SetHttpClient(&http.Client{Transport: args.Transport})} 263 // Set basic auth 264 if args.Username != "" && args.Password != "" { 265 options = append(options, elastic.SetBasicAuth(args.Username, args.Password)) 266 } 267 // Create a client 268 client, err := elastic.NewClient(options...) 269 if err != nil { 270 // https://github.com/olivere/elastic/wiki/Connection-Errors 271 if elastic.IsConnErr(err) || elastic.IsContextErr(err) || xnet.IsNetworkOrHostDown(err, false) { 272 return nil, errNotConnected 273 } 274 return nil, err 275 } 276 if err = createIndex(client, args); err != nil { 277 return nil, err 278 } 279 return client, nil 280 } 281 282 // NewElasticsearchTarget - creates new Elasticsearch target. 283 func NewElasticsearchTarget(id string, args ElasticsearchArgs, doneCh <-chan struct{}, loggerOnce func(ctx context.Context, err error, id interface{}, kind ...interface{}), test bool) (*ElasticsearchTarget, error) { 284 target := &ElasticsearchTarget{ 285 id: event.TargetID{ID: id, Name: "elasticsearch"}, 286 args: args, 287 loggerOnce: loggerOnce, 288 } 289 290 if args.QueueDir != "" { 291 queueDir := filepath.Join(args.QueueDir, storePrefix+"-elasticsearch-"+id) 292 target.store = NewQueueStore(queueDir, args.QueueLimit) 293 if err := target.store.Open(); err != nil { 294 target.loggerOnce(context.Background(), err, target.ID()) 295 return target, err 296 } 297 } 298 299 var err error 300 target.client, err = newClient(args) 301 if err != nil { 302 if target.store == nil || err != errNotConnected { 303 target.loggerOnce(context.Background(), err, target.ID()) 304 return target, err 305 } 306 } 307 308 if target.store != nil && !test { 309 // Replays the events from the store. 310 eventKeyCh := replayEvents(target.store, doneCh, target.loggerOnce, target.ID()) 311 // Start replaying events from the store. 312 go sendEvents(target, eventKeyCh, doneCh, target.loggerOnce) 313 } 314 315 return target, nil 316 }