.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | [![Contributors][contributors-shield]][contributors-url]
5 | [![Forks][forks-shield]][forks-url]
6 | [![Stargazers][stars-shield]][stars-url]
7 | [![Issues][issues-shield]][issues-url]
8 | [![MIT License][license-shield]][license-url]
9 | ![Compatibility][compatibility-shield]
10 | [![Xing][xing-shield]][xing-url]
11 |
12 |
13 | # BBB-Readable-Feedback
14 |
15 | Did you activate the
16 | [feedback feature](https://docs.bigbluebutton.org/admin/customize.html#collect-feedback-from-the-users)
17 | in BigBlueButton (BBB) but you can't read all the jibberish in the logfile?
18 | This simple script creates humanly readable output from the feedback logs of your BigBlueButton (BBB) instance.
19 |
20 | It has 7 simple cmd parameters you **can** use, none of which is required:
21 |
22 | | Short | Parameter | Default | Explanation |
23 | | :--- | :--- | :--- | :--- |
24 | | -h | --help | None | Displays this table
25 | | -p | --path | /var/log/nginx/ | Provide the full path to the directory containing the feedback logs |
26 | | -cl | --charlimit | 100 | The character length on which we wrap the comment |
27 | | -s | --silent | False | If True the script won't have any output |
28 | | -pz | --parsezip | False | If True unzip `.gz` logs and parse them aswell |
29 | | -tf | --tofile | False | If True write the output to `html5-client-readable.log` |
30 | | -csv | | False | If True write the output to `BBB-Feedback.csv` |
31 |
32 |
33 | ## Installation
34 |
35 | * Make sure you got atleast `Python 3.6` installed: `python3 -V`
36 | * Copy the script or clone this repo to the server containing the BBB instance:
37 | `git clone https://github.com/Helyux/BBB-Readable-Feedback`
38 |
39 | ## Example Use
40 |
41 | Execute `python3 ReadFeedback.py -pz -tf`
42 |
43 | This will parse all `html5-client.log` files in the directory `/var/log/nginx/`, even those who are already
44 | zipped (`-pz`) and display something like this:
45 | 
46 | It'll also write the displayed info to the file
47 | `html5-client-readable.log` in the given path. (`-tf`)
48 |
49 |
50 |
51 | [contributors-shield]: https://img.shields.io/github/contributors/Helyux/BBB-Readable-Feedback.svg?style=for-the-badge
52 | [contributors-url]: https://github.com/Helyux/BBB-Readable-Feedback/graphs/contributors
53 | [forks-shield]: https://img.shields.io/github/forks/Helyux/BBB-Readable-Feedback.svg?style=for-the-badge
54 | [forks-url]: https://github.com/Helyux/BBB-Readable-Feedback/network/members
55 | [stars-shield]: https://img.shields.io/github/stars/Helyux/BBB-Readable-Feedback.svg?style=for-the-badge
56 | [stars-url]: https://github.com/Helyux/BBB-Readable-Feedback/stargazers
57 | [issues-shield]: https://img.shields.io/github/issues/Helyux/BBB-Readable-Feedback.svg?style=for-the-badge
58 | [issues-url]: https://github.com/Helyux/BBB-Readable-Feedback/issues
59 | [license-shield]: https://img.shields.io/github/license/Helyux/BBB-Readable-Feedback.svg?style=for-the-badge
60 | [license-url]: https://github.com/Helyux/BBB-Readable-Feedback/blob/master/LICENSE
61 | [xing-shield]: https://img.shields.io/static/v1?style=for-the-badge&message=Xing&color=006567&logo=Xing&logoColor=FFFFFF&label
62 | [xing-url]: https://www.xing.com/profile/Lukas_Mahler10
63 | [compatibility-shield]: https://img.shields.io/badge/bbb--compatibility-2.3%20%7C%202.4%20%7C%202.5-success?style=for-the-badge
--------------------------------------------------------------------------------
/ReadFeedback.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Parses the feedback logfiles created by a BBB Instance and makes them humanly readable.
5 | To enable feedback logs: https://docs.bigbluebutton.org/admin/customize.html#collect-feedback-from-the-users
6 | """
7 |
8 | __author__ = "Lukas Mahler"
9 | __version__ = "0.4.0"
10 | __date__ = "26.09.2022"
11 | __email__ = "m@hler.eu"
12 | __status__ = "Development"
13 |
14 | import csv
15 | import gzip
16 | import shutil
17 | import textwrap
18 | from pathlib import Path
19 | from datetime import datetime
20 | from typing import Tuple, Dict, List
21 | from argparse import ArgumentParser, Namespace
22 |
23 |
24 | def parsefeedback(args: Namespace) -> Tuple[List[Dict], Dict]:
25 |
26 | logspath = args.path
27 | parsezip = args.parsezip
28 | silent = args.silent
29 |
30 | data = []
31 | myrating = 0
32 | numratings = 0
33 |
34 | if not silent:
35 | print("[x] Started parsing feedback logs\n")
36 |
37 | # Find all (rotated) logfiles
38 | logs = list(logspath.glob("html5-client.log*"))
39 |
40 | # No html5-client.log found
41 | if len(logs) == 0:
42 | raise FileNotFoundError("[x] No logfiles found")
43 |
44 | for index, log in enumerate(logs):
45 |
46 | if not silent:
47 | print(f"[{index+1}/{len(logs)}] parsed")
48 |
49 | unzipped = False
50 | if log.suffix == ".gz":
51 | if parsezip:
52 | with gzip.open(log, 'rb') as zipin:
53 | with open(log.with_suffix(''), 'wb') as zipout:
54 | shutil.copyfileobj(zipin, zipout)
55 | log = Path(zipout.name)
56 | unzipped = True
57 | else:
58 | continue
59 |
60 | with open(log, 'r') as readfile:
61 | for line in readfile:
62 | # Get good UTF-8 encoding
63 | line = bytes(line, encoding='latin1').decode('unicode_escape')
64 |
65 | # Skip empty lines
66 | try:
67 | line = line.split(r" [")[2][:-2]
68 | except IndexError:
69 | continue
70 |
71 | # String formatting
72 | line = line.replace(r"\n", " ")
73 | line = line.replace("}", "")
74 | line = line.replace("{", "")
75 | line = line.replace(",", ":")
76 | line = line.replace('"', '')
77 | line = line.split(":")
78 |
79 | # Read the timestamp
80 | if "time" in line:
81 | start = line.index("time")
82 | time = "".join(line[start + 1:start + 3])
83 | timestamp = datetime.strptime(time, "%Y-%m-%dT%H%M").strftime("%d.%m.%y %H:%M")
84 |
85 | # Read the given rating
86 | if "rating" in line:
87 | start = line.index("rating")
88 | rating = "".join(line[start + 1:start + 2])
89 |
90 | myrating = myrating + int(rating)
91 | numratings += 1
92 |
93 | # Read the commenters name
94 | if "fullname" in line:
95 | start = line.index("fullname")
96 | name = ",".join(line[start + 1:start + 3])
97 | name = bytes(name, encoding='latin1').decode('UTF-8')
98 | if "confname" in name:
99 | # Non registered accounts will have "confname" in their name.
100 | # Replace these with (temp)
101 | name = name.replace(",confname", " (temp)")
102 |
103 | # Split comment every args.charlimit (default = 100) characters
104 | if "comment" in line:
105 | start = line.index("comment") + 1
106 | end = line.index("userRole")
107 | raw_comment = "".join(line[start:end])
108 | raw_comment = bytes(raw_comment, encoding='latin1').decode('UTF-8')
109 |
110 | comment = f"\n{'':15s}│ {'':8s}│ {'':30s}│ ".join(textwrap.wrap(raw_comment, args.charlimit))
111 |
112 | # Add good comments to dict
113 | if "comment" in line:
114 | data.append({
115 | 'timestamp': timestamp,
116 | 'rating': rating + " Stars",
117 | 'name': name,
118 | 'comment': comment,
119 | 'raw_comment': raw_comment
120 | })
121 |
122 | # Delete unzipped files
123 | if unzipped:
124 | log.unlink()
125 |
126 | # Sort data by timestamp
127 | sorted_data = sorted(data, key=lambda k: datetime.strptime(k['timestamp'], "%d.%m.%y %H:%M"))
128 |
129 | # Calculate rating_data
130 | if numratings == 0:
131 | median = 0
132 | else:
133 | median = round(myrating/numratings, 2)
134 | rating_data = {'num': numratings, 'median': median}
135 |
136 | if not silent:
137 | print("\n[x] Finished parsing feedback logs")
138 |
139 | return sorted_data, rating_data
140 |
141 |
142 | def print_parsed(args: Namespace, data: List[Dict], rating: Dict) -> None:
143 | print(f"\n{'Timestamp:':15s}│ {'Rating:':8s}│ {'Author:':30s}│ Comment:")
144 | print(f"{15*'─'}┼{9*'─'}┼{31*'─'}┼{(args.charlimit + 2)*'─'}")
145 | lastline = len(data)-1
146 | for index, entry in enumerate(data):
147 | print(f"{entry['timestamp']:15}│ {entry['rating']:8}│ {entry['name']:30s}│ {entry['comment']}")
148 | if index != lastline:
149 | print(f"{15*'─'}┼{9 *'─'}┼{31*'─'}┼{(args.charlimit + 2)*'─'}")
150 | else:
151 | print(f"{15*'─'}┴{9*'─'}┴{31*'─'}┴{(args.charlimit + 2)*'─'}\n")
152 | print(f"Median rating: {rating['median']} with {rating['num']} Votes\n")
153 |
154 |
155 | def write_parsed(args: Namespace, data: List[Dict], rating: Dict) -> None:
156 | logspath = args.path
157 | silent = args.silent
158 |
159 | writefile = logspath / "html5-client-readable.log"
160 | with open(writefile, 'w', encoding='UTF-8') as f:
161 | f.write(f"{'Timestamp:':15s}│ {'Rating:':8s}│ {'Author':30s}│ Comment:\n")
162 | f.write(f"{15*'─'}┼{9*'─'}┼{31*'─'}┼{(args.charlimit + 2)*'─'}\n")
163 | lastline = len(data) - 1
164 | for index, entry in enumerate(data):
165 | f.write(f"{entry['timestamp']:15}│ {entry['rating']:8}│ {entry['name']:30s}│ {entry['comment']}\n")
166 | if index != lastline:
167 | f.write(f"{15*'─'}┼{9*'─'}┼{31*'─'}┼{(args.charlimit + 2)*'─'}\n")
168 | else:
169 | f.write(f"{15*'─'}┴{9*'─'}┴{31*'─'}┴{(args.charlimit + 2)*'─'}\n")
170 | f.write(f"Median rating: {rating['median']} with {rating['num']} Votes")
171 |
172 | if not silent:
173 | print(f"→ Wrote to file: {writefile.name}")
174 |
175 |
176 | def write_csv(args: Namespace, data: List[Dict]) -> None:
177 | logspath = args.path
178 | silent = args.silent
179 |
180 | writefile = logspath / "BBB-Feedback.csv"
181 | with open(writefile, 'w', encoding='UTF-8', newline='') as f:
182 | write = csv.writer(f)
183 | for entry in data:
184 | write.writerow([entry['timestamp'], entry['rating'], entry['name'], entry['raw_comment']])
185 |
186 | if not silent:
187 | print(f"→ Wrote to file: {writefile.name}")
188 |
189 |
190 | # ======================================================================================================================
191 |
192 | def main():
193 |
194 | # Parse cmd parameter
195 | parser = ArgumentParser()
196 | parser.add_argument('-p', '--path', default='/var/log/nginx/', type=Path,
197 | help='Provide the full path to the directory containing the feedback logs')
198 | parser.add_argument('-cl', '--charlimit', default=100, type=int,
199 | help='The character length on which we wrap the comment')
200 | parser.add_argument('-s', '--silent', action='store_true',
201 | help="If True the script won't have any output")
202 | parser.add_argument('-tf', '--tofile', action='store_true',
203 | help='If True parse the output to `html5-client-readable.log`')
204 | parser.add_argument('-csv', action='store_true',
205 | help='If True parse the output into `feedback.csv`')
206 | parser.add_argument('-pz', '--parsezip', action='store_true',
207 | help='If True unzip .gz logs and parse them aswell')
208 | args = parser.parse_args()
209 |
210 | silent = args.silent
211 |
212 | # Execute feedback parsing
213 | data, rating = parsefeedback(args)
214 | if not silent:
215 | print_parsed(args, data, rating)
216 | if args.tofile:
217 | write_parsed(args, data, rating)
218 | if args.csv:
219 | write_csv(args, data)
220 |
221 |
222 | if __name__ == "__main__":
223 | main()
224 |
--------------------------------------------------------------------------------