github.com/argoproj/argo-events@v1.9.1/eventsources/sources/file/start.go (about)

     1  /*
     2  Copyright 2018 BlackRock, 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 file
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"regexp"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/fsnotify/fsnotify"
    28  	watcherpkg "github.com/radovskyb/watcher"
    29  	"go.uber.org/zap"
    30  
    31  	"github.com/argoproj/argo-events/common/logging"
    32  	eventsourcecommon "github.com/argoproj/argo-events/eventsources/common"
    33  	"github.com/argoproj/argo-events/eventsources/common/fsevent"
    34  	"github.com/argoproj/argo-events/eventsources/sources"
    35  	metrics "github.com/argoproj/argo-events/metrics"
    36  	apicommon "github.com/argoproj/argo-events/pkg/apis/common"
    37  	"github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1"
    38  )
    39  
    40  // EventListener implements Eventing for file event source
    41  type EventListener struct {
    42  	EventSourceName string
    43  	EventName       string
    44  	FileEventSource v1alpha1.FileEventSource
    45  	Metrics         *metrics.Metrics
    46  }
    47  
    48  // GetEventSourceName returns name of event source
    49  func (el *EventListener) GetEventSourceName() string {
    50  	return el.EventSourceName
    51  }
    52  
    53  // GetEventName returns name of event
    54  func (el *EventListener) GetEventName() string {
    55  	return el.EventName
    56  }
    57  
    58  // GetEventSourceType return type of event server
    59  func (el *EventListener) GetEventSourceType() apicommon.EventSourceType {
    60  	return apicommon.FileEvent
    61  }
    62  
    63  // StartListening starts listening events
    64  func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error {
    65  	log := logging.FromContext(ctx).
    66  		With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName())
    67  	defer sources.Recover(el.GetEventName())
    68  
    69  	fileEventSource := &el.FileEventSource
    70  	if fileEventSource.Polling {
    71  		if err := el.listenEventsPolling(ctx, dispatch, log); err != nil {
    72  			log.Error("failed to listen to events", zap.Error(err))
    73  			return err
    74  		}
    75  	} else {
    76  		if err := el.listenEvents(ctx, dispatch, log); err != nil {
    77  			log.Error("failed to listen to events", zap.Error(err))
    78  			return err
    79  		}
    80  	}
    81  	return nil
    82  }
    83  
    84  // listenEvents listen to file related events.
    85  func (el *EventListener) listenEvents(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error, log *zap.SugaredLogger) error {
    86  	fileEventSource := &el.FileEventSource
    87  
    88  	// create new fs watcher
    89  	log.Info("setting up a new file watcher...")
    90  	watcher, err := fsnotify.NewWatcher()
    91  	if err != nil {
    92  		return fmt.Errorf("failed to set up a file watcher for %s, %w", el.GetEventName(), err)
    93  	}
    94  	defer watcher.Close()
    95  
    96  	// file descriptor to watch must be available in file system. You can't watch an fs descriptor that is not present.
    97  	log.Info("adding directory to monitor for the watcher...")
    98  	err = watcher.Add(fileEventSource.WatchPathConfig.Directory)
    99  	if err != nil {
   100  		return fmt.Errorf("failed to add directory %s to the watcher for %s, %w", fileEventSource.WatchPathConfig.Directory, el.GetEventName(), err)
   101  	}
   102  
   103  	var pathRegexp *regexp.Regexp
   104  	if fileEventSource.WatchPathConfig.PathRegexp != "" {
   105  		log.Infow("matching file path with configured regex...", zap.Any("regex", fileEventSource.WatchPathConfig.PathRegexp))
   106  		pathRegexp, err = regexp.Compile(fileEventSource.WatchPathConfig.PathRegexp)
   107  		if err != nil {
   108  			return fmt.Errorf("failed to match file path with configured regex %s for %s, %w", fileEventSource.WatchPathConfig.PathRegexp, el.GetEventName(), err)
   109  		}
   110  	}
   111  
   112  	processOne := func(event fsnotify.Event) error {
   113  		defer func(start time.Time) {
   114  			el.Metrics.EventProcessingDuration(el.GetEventSourceName(), el.GetEventName(), float64(time.Since(start)/time.Millisecond))
   115  		}(time.Now())
   116  
   117  		log.Infow("file event", zap.Any("event-type", event.Op.String()), zap.Any("descriptor-name", event.Name))
   118  
   119  		// Assume fsnotify event has the same Op spec of our file event
   120  		fileEvent := fsevent.Event{Name: event.Name, Op: fsevent.NewOp(event.Op.String()), Metadata: el.FileEventSource.Metadata}
   121  		payload, err := json.Marshal(fileEvent)
   122  		if err != nil {
   123  			return fmt.Errorf("failed to marshal the event to the fs event, %w", err)
   124  		}
   125  		log.Infow("dispatching file event on data channel...", zap.Any("event-type", event.Op.String()), zap.Any("descriptor-name", event.Name))
   126  		if err = dispatch(payload); err != nil {
   127  			return fmt.Errorf("failed to dispatch a file event, %w", err)
   128  		}
   129  		return nil
   130  	}
   131  
   132  	log.Info("listening to file notifications...")
   133  	for {
   134  		select {
   135  		case event, ok := <-watcher.Events:
   136  			if !ok {
   137  				log.Info("fs watcher has stopped")
   138  				// watcher stopped watching file events
   139  				return fmt.Errorf("fs watcher stopped for %s", el.GetEventName())
   140  			}
   141  			// fwc.Path == event.Name is required because we don't want to send event when .swp files are created
   142  			matched := false
   143  			relPath := strings.TrimPrefix(event.Name, fileEventSource.WatchPathConfig.Directory)
   144  			if fileEventSource.WatchPathConfig.Path != "" && fileEventSource.WatchPathConfig.Path == relPath {
   145  				matched = true
   146  			} else if pathRegexp != nil && pathRegexp.MatchString(relPath) {
   147  				matched = true
   148  			}
   149  			if matched && fileEventSource.EventType == event.Op.String() {
   150  				if err = processOne(event); err != nil {
   151  					log.Errorw("failed to process a file event", zap.Error(err))
   152  					el.Metrics.EventProcessingFailed(el.GetEventSourceName(), el.GetEventName())
   153  				}
   154  			}
   155  		case err := <-watcher.Errors:
   156  			return fmt.Errorf("failed to process %s, %w", el.GetEventName(), err)
   157  		case <-ctx.Done():
   158  			log.Info("event source has been stopped")
   159  			return nil
   160  		}
   161  	}
   162  }
   163  
   164  // listenEvents listen to file related events using polling.
   165  func (el *EventListener) listenEventsPolling(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error, log *zap.SugaredLogger) error {
   166  	fileEventSource := &el.FileEventSource
   167  
   168  	// create new fs watcher
   169  	log.Info("setting up a new file polling watcher...")
   170  	watcher := watcherpkg.New()
   171  	defer watcher.Close()
   172  
   173  	// file descriptor to watch must be available in file system. You can't watch an fs descriptor that is not present.
   174  	log.Info("adding directory to monitor for the watcher...")
   175  	err := watcher.Add(fileEventSource.WatchPathConfig.Directory)
   176  	if err != nil {
   177  		return fmt.Errorf("failed to add directory %s to the watcher for %s, %w", fileEventSource.WatchPathConfig.Directory, el.GetEventName(), err)
   178  	}
   179  
   180  	var pathRegexp *regexp.Regexp
   181  	if fileEventSource.WatchPathConfig.PathRegexp != "" {
   182  		log.Infow("matching file path with configured regex...", zap.Any("regex", fileEventSource.WatchPathConfig.PathRegexp))
   183  		pathRegexp, err = regexp.Compile(fileEventSource.WatchPathConfig.PathRegexp)
   184  		if err != nil {
   185  			return fmt.Errorf("failed to match file path with configured regex %s for %s, %w", fileEventSource.WatchPathConfig.PathRegexp, el.GetEventName(), err)
   186  		}
   187  	}
   188  
   189  	processOne := func(event watcherpkg.Event) error {
   190  		defer func(start time.Time) {
   191  			el.Metrics.EventProcessingDuration(el.GetEventSourceName(), el.GetEventName(), float64(time.Since(start)/time.Millisecond))
   192  		}(time.Now())
   193  
   194  		log.Infow("file event", zap.Any("event-type", event.Op.String()), zap.Any("descriptor-name", event.Name))
   195  
   196  		// Assume fsnotify event has the same Op spec of our file event
   197  		fileEvent := fsevent.Event{Name: event.Name(), Op: fsevent.NewOp(event.Op.String()), Metadata: el.FileEventSource.Metadata}
   198  		payload, err := json.Marshal(fileEvent)
   199  		if err != nil {
   200  			return fmt.Errorf("failed to marshal the event to the fs event, %w", err)
   201  		}
   202  		log.Infow("dispatching file event on data channel...", zap.Any("event-type", event.Op.String()), zap.Any("descriptor-name", event.Name))
   203  		if err = dispatch(payload); err != nil {
   204  			return fmt.Errorf("failed to dispatch file event, %w", err)
   205  		}
   206  		return nil
   207  	}
   208  
   209  	go func() {
   210  		log.Info("listening to file notifications...")
   211  		for {
   212  			select {
   213  			case event, ok := <-watcher.Event:
   214  				if !ok {
   215  					log.Info("fs watcher has stopped")
   216  					// watcher stopped watching file events
   217  					log.Errorw("fs watcher stopped", zap.Any("eventName", el.GetEventName()))
   218  					return
   219  				}
   220  				// fwc.Path == event.Name is required because we don't want to send event when .swp files are created
   221  				matched := false
   222  				relPath := strings.TrimPrefix(event.Name(), fileEventSource.WatchPathConfig.Directory)
   223  				if fileEventSource.WatchPathConfig.Path != "" && fileEventSource.WatchPathConfig.Path == relPath {
   224  					matched = true
   225  				} else if pathRegexp != nil && pathRegexp.MatchString(relPath) {
   226  					matched = true
   227  				}
   228  				if matched && fileEventSource.EventType == event.Op.String() {
   229  					if err := processOne(event); err != nil {
   230  						log.Errorw("failed to process a file event", zap.Error(err))
   231  						el.Metrics.EventProcessingFailed(el.GetEventSourceName(), el.GetEventName())
   232  					}
   233  				}
   234  			case err := <-watcher.Error:
   235  				log.Errorw("failed to process event source", zap.Any("eventName", el.GetEventName()), zap.Error(err))
   236  				return
   237  			case <-ctx.Done():
   238  				log.Info("event source has been stopped")
   239  				return
   240  			}
   241  		}
   242  	}()
   243  	log.Info("Starting watcher...")
   244  	if err = watcher.Start(time.Millisecond * 100); err != nil {
   245  		return fmt.Errorf("Failed to start watcher for %s, %w", el.GetEventName(), err)
   246  	}
   247  	return nil
   248  }