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 }