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  }