Skip to content

Commit c5b254c

Browse files
committed
add watchdog tutorial
1 parent 707a12b commit c5b254c

File tree

5 files changed

+289
-0
lines changed

5 files changed

+289
-0
lines changed

general/directory-watcher/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# [How to Create a Watchdog in Python](https://www.thepythoncode.com/article/create-a-watchdog-in-python)
2+
To run this:
3+
- `pip3 install -r requirements.txt`
4+
- `python3 controller.py --help`
5+
**Output:**
6+
```
7+
usage: controller.py [-h] [-d WATCH_DELAY] [-r] [-p PATTERN] [--watch-directories] path
8+
9+
Watchdog script for watching for files & directories' changes
10+
11+
positional arguments:
12+
path
13+
14+
optional arguments:
15+
-h, --help show this help message and exit
16+
-d WATCH_DELAY, --watch-delay WATCH_DELAY
17+
Watch delay, default is 1
18+
-r, --recursive Whether to recursively watch for the path's children, default is False
19+
-p PATTERN, --pattern PATTERN
20+
Pattern of files to watch, default is .txt,.trc,.log
21+
--watch-directories Whether to watch directories, default is True
22+
```
23+
- For example, watching the path `E:\watchdog` recursively for log and text files:
24+
```
25+
python controller.py E:\watchdog --recursive -p .txt,.log
26+
```

general/directory-watcher/checker.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import datetime
2+
from pygtail import Pygtail
3+
4+
# Loading the package called re from the RegEx Module in order to work with Regular Expressions
5+
import re
6+
7+
8+
class FileChecker:
9+
def __init__(self, exceptionPattern):
10+
self.exceptionPattern = exceptionPattern
11+
12+
def checkForException(self, event, path):
13+
# Get current date and time according to the specified format.
14+
now = (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
15+
# Read the lines of the file (specified in the path) that have not been read yet
16+
# Meaning by that it will start from the point where it was last stopped.
17+
for num, line in enumerate(Pygtail(path), 1):
18+
# Remove leading and trailing whitespaces including newlines.
19+
line = line.strip()
20+
# Return all non-overlapping matches of the values specified in the Exception Pattern.
21+
# The line is scanned from left to right and matches are returned in the oder found.
22+
if line and any(re.findall('|'.join(self.exceptionPattern), line, flags=re.I | re.X)):
23+
# Observation Detected
24+
type = 'observation'
25+
msg = f"{now} -- {event.event_type} -- File: {path} -- Observation: {line}"
26+
yield type, msg
27+
elif line:
28+
# No Observation Detected
29+
type = 'msg'
30+
msg = f"{now} -- {event.event_type} -- File: {path}"
31+
yield type, msg

general/directory-watcher/config.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Application configuration File
2+
################################
3+
4+
# Directory To Watch, If not specified, the following value will be considered explicitly.
5+
WATCH_DIRECTORY = "C:\\SCRIPTS"
6+
7+
# Delay Between Watch Cycles In Seconds
8+
WATCH_DELAY = 1
9+
10+
# Check The WATCH_DIRECTORY and its children
11+
WATCH_RECURSIVELY = False
12+
13+
# whether to watch for directory events
14+
DO_WATCH_DIRECTORIES = True
15+
16+
# Patterns of the files to watch
17+
WATCH_PATTERN = '.txt,.trc,.log'
18+
19+
LOG_FILES_EXTENSIONS = ('.txt', '.log', '.trc')
20+
21+
# Patterns for observations
22+
EXCEPTION_PATTERN = ['EXCEPTION', 'FATAL', 'ERROR']
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# The Observer watches for any file change and then dispatches the respective events to an event handler.
2+
from watchdog.observers import Observer
3+
# The event handler will be notified when an event occurs.
4+
from watchdog.events import FileSystemEventHandler
5+
import time
6+
import config
7+
import os
8+
from checker import FileChecker
9+
import datetime
10+
from colorama import Fore, Style, init
11+
12+
init()
13+
14+
GREEN = Fore.GREEN
15+
BLUE = Fore.BLUE
16+
RED = Fore.RED
17+
YELLOW = Fore.YELLOW
18+
19+
event2color = {
20+
"created": GREEN,
21+
"modified": BLUE,
22+
"deleted": RED,
23+
"moved": YELLOW,
24+
}
25+
26+
27+
def print_with_color(s, color=Fore.WHITE, brightness=Style.NORMAL, **kwargs):
28+
"""Utility function wrapping the regular `print()` function
29+
but with colors and brightness"""
30+
print(f"{brightness}{color}{s}{Style.RESET_ALL}", **kwargs)
31+
32+
33+
# Class that inherits from FileSystemEventHandler for handling the events sent by the Observer
34+
class LogHandler(FileSystemEventHandler):
35+
36+
def __init__(self, watchPattern, exceptionPattern, doWatchDirectories):
37+
self.watchPattern = watchPattern
38+
self.exceptionPattern = exceptionPattern
39+
self.doWatchDirectories = doWatchDirectories
40+
# Instantiate the checker
41+
self.fc = FileChecker(self.exceptionPattern)
42+
43+
def on_any_event(self, event):
44+
now = (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
45+
# print("event happened:", event)
46+
# To Observe files only not directories
47+
if not event.is_directory:
48+
# To cater for the on_move event
49+
path = event.src_path
50+
if hasattr(event, 'dest_path'):
51+
path = event.dest_path
52+
# Ensure that the file extension is among the pre-defined ones.
53+
if path.endswith(self.watchPattern):
54+
msg = f"{now} -- {event.event_type} -- File: {path}"
55+
if event.event_type in ('modified', 'created', 'moved'):
56+
# check for exceptions in log files
57+
if path.endswith(config.LOG_FILES_EXTENSIONS):
58+
for type, msg in self.fc.checkForException(event=event, path=path):
59+
print_with_color(
60+
msg, color=event2color[event.event_type], brightness=Style.BRIGHT)
61+
else:
62+
print_with_color(
63+
msg, color=event2color[event.event_type])
64+
else:
65+
print_with_color(msg, color=event2color[event.event_type])
66+
elif self.doWatchDirectories:
67+
msg = f"{now} -- {event.event_type} -- Folder: {event.src_path}"
68+
print_with_color(msg, color=event2color[event.event_type])
69+
70+
def on_modified(self, event):
71+
pass
72+
73+
def on_deleted(self, event):
74+
pass
75+
76+
def on_created(self, event):
77+
pass
78+
79+
def on_moved(self, event):
80+
pass
81+
82+
83+
class LogWatcher:
84+
# Initialize the observer
85+
observer = None
86+
# Initialize the stop signal variable
87+
stop_signal = 0
88+
89+
# The observer is the class that watches for any file system change and then dispatches the event to the event handler.
90+
def __init__(self, watchDirectory, watchDelay, watchRecursively, watchPattern, doWatchDirectories, exceptionPattern):
91+
# Initialize variables in relation
92+
self.watchDirectory = watchDirectory
93+
self.watchDelay = watchDelay
94+
self.watchRecursively = watchRecursively
95+
self.watchPattern = watchPattern
96+
self.doWatchDirectories = doWatchDirectories
97+
self.exceptionPattern = exceptionPattern
98+
99+
# Create an instance of watchdog.observer
100+
self.observer = Observer()
101+
# The event handler is an object that will be notified when something happens to the file system.
102+
self.event_handler = LogHandler(
103+
watchPattern, exceptionPattern, self.doWatchDirectories)
104+
105+
def schedule(self):
106+
print("Observer Scheduled:", self.observer.name)
107+
# Call the schedule function via the Observer instance attaching the event
108+
self.observer.schedule(
109+
self.event_handler, self.watchDirectory, recursive=self.watchRecursively)
110+
111+
def start(self):
112+
print("Observer Started:", self.observer.name)
113+
self.schedule()
114+
# Start the observer thread and wait for it to generate events
115+
now = (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
116+
msg = f"Observer: {self.observer.name} - Started On: {now}"
117+
print(msg)
118+
119+
msg = (
120+
f"Watching {'Recursively' if self.watchRecursively else 'Non-Recursively'}: {self.watchPattern}"
121+
f" -- Folder: {self.watchDirectory} -- Every: {self.watchDelay}(sec) -- For Patterns: {self.exceptionPattern}"
122+
)
123+
print(msg)
124+
self.observer.start()
125+
126+
def run(self):
127+
print("Observer is running:", self.observer.name)
128+
self.start()
129+
try:
130+
while True:
131+
time.sleep(self.watchDelay)
132+
133+
if self.stop_signal == 1:
134+
print(
135+
f"Observer stopped: {self.observer.name} stop signal:{self.stop_signal}")
136+
self.stop()
137+
break
138+
except:
139+
self.stop()
140+
self.observer.join()
141+
142+
def stop(self):
143+
print("Observer Stopped:", self.observer.name)
144+
145+
now = (datetime.datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
146+
msg = f"Observer: {self.observer.name} - Stopped On: {now}"
147+
print(msg)
148+
self.observer.stop()
149+
self.observer.join()
150+
151+
def info(self):
152+
info = {
153+
'observerName': self.observer.name,
154+
'watchDirectory': self.watchDirectory,
155+
'watchDelay': self.watchDelay,
156+
'watchRecursively': self.watchRecursively,
157+
'watchPattern': self.watchPattern,
158+
}
159+
return info
160+
161+
162+
def is_dir_path(path):
163+
"""Utility function to check whether a path is an actual directory"""
164+
if os.path.isdir(path):
165+
return path
166+
else:
167+
raise NotADirectoryError(path)
168+
169+
170+
if __name__ == "__main__":
171+
import argparse
172+
parser = argparse.ArgumentParser(
173+
description="Watchdog script for watching for files & directories' changes")
174+
parser.add_argument("path",
175+
default=config.WATCH_DIRECTORY,
176+
type=is_dir_path,
177+
)
178+
parser.add_argument("-d", "--watch-delay",
179+
help=f"Watch delay, default is {config.WATCH_DELAY}",
180+
default=config.WATCH_DELAY,
181+
type=int,
182+
)
183+
parser.add_argument("-r", "--recursive",
184+
action="store_true",
185+
help=f"Whether to recursively watch for the path's children, default is {config.WATCH_RECURSIVELY}",
186+
default=config.WATCH_RECURSIVELY,
187+
)
188+
parser.add_argument("-p", "--pattern",
189+
help=f"Pattern of files to watch, default is {config.WATCH_PATTERN}",
190+
default=config.WATCH_PATTERN,
191+
)
192+
parser.add_argument("--watch-directories",
193+
action="store_true",
194+
help=f"Whether to watch directories, default is {config.DO_WATCH_DIRECTORIES}",
195+
default=config.DO_WATCH_DIRECTORIES,
196+
)
197+
# parse the arguments
198+
args = parser.parse_args()
199+
# define & launch the log watcher
200+
log_watcher = LogWatcher(
201+
watchDirectory=args.path,
202+
watchDelay=args.watch_delay,
203+
watchRecursively=args.recursive,
204+
watchPattern=tuple(args.pattern.split(",")),
205+
doWatchDirectories=args.watch_directories,
206+
exceptionPattern=config.EXCEPTION_PATTERN,
207+
)
208+
log_watcher.run()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Pygtail==0.11.1
2+
watchdog==2.1.1

0 commit comments

Comments
 (0)