radicale 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # This file is related to Radicale - CalDAV and CardDAV server
  2. # for logwatch (script)
  3. # Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
  4. #
  5. # Detail levels
  6. # < 5 : Request + ResponseCounters
  7. # >= 5 : incl. Logins
  8. # >= 10: incl. ResponseTimes + ResponseSize
  9. # >= 15: incl. ResponseTimes + ResponseSize incl. RequestFlags
  10. # >= 18: incl. UserAgents
  11. # >= 20: incl. Locations where supported, anonymize logins
  12. use Digest::SHA;
  13. $Detail = $ENV{'LOGWATCH_DETAIL_LEVEL'} || 0;
  14. my %ResponseTimesLocUsr;
  15. my %ResponseSizesLocUsr;
  16. my %ResponseTimes;
  17. my %ResponseSizes;
  18. my %Responses;
  19. my %Requests;
  20. my %UserAgents;
  21. my %Logins;
  22. my %Loglevel;
  23. my %OtherEvents;
  24. my %Locations;
  25. my %LocationsFile;
  26. my %LoginsHash;
  27. my $sum;
  28. my $length;
  29. sub ResponseTimesMinMaxSum($$) {
  30. my $req = $_[0];
  31. my $time = $_[1];
  32. $ResponseTimes{$req}->{'cnt'}++;
  33. if (! defined $ResponseTimes{$req}->{'min'}) {
  34. $ResponseTimes{$req}->{'min'} = $time;
  35. } elsif ($ResponseTimes{$req}->{'min'} > $time) {
  36. $ResponseTimes{$req}->{'min'} = $time;
  37. }
  38. if (! defined $ResponseTimes{$req}->{'max'}) {
  39. $ResponseTimes{$req}{'max'} = $time;
  40. } elsif ($ResponseTimes{$req}->{'max'} < $time) {
  41. $ResponseTimes{$req}{'max'} = $time;
  42. }
  43. $ResponseTimes{$req}->{'sum'} += $time;
  44. }
  45. sub ResponseSizesMinMaxSum($$$) {
  46. my $req = $_[0];
  47. my $type = $_[1];
  48. my $size = $_[2];
  49. $ResponseSizes{$type}->{$req}->{'cnt'}++;
  50. if (! defined $ResponseSizes{$type}->{$req}->{'min'}) {
  51. $ResponseSizes{$type}->{$req}->{'min'} = $size;
  52. } elsif ($ResponseSizes{$type}->{$req}->{'min'} > $size) {
  53. $ResponseSizes{$type}->{$req}->{'min'} = $size;
  54. }
  55. if (! defined $ResponseSizes{$type}->{$req}->{'max'}) {
  56. $ResponseSizes{$type}->{$req}{'max'} = $size;
  57. } elsif ($ResponseSizes{$type}->{$req}->{'max'} < $size) {
  58. $ResponseSizes{$type}->{$req}{'max'} = $size;
  59. }
  60. $ResponseSizes{$type}->{$req}->{'sum'} += $size;
  61. }
  62. sub Sum($) {
  63. my $phash = $_[0];
  64. my $sum = 0;
  65. foreach my $entry (keys %$phash) {
  66. $sum += $phash->{$entry};
  67. }
  68. return $sum;
  69. }
  70. sub MaxLength($) {
  71. my $phash = $_[0];
  72. my $length = 0;
  73. foreach my $entry (keys %$phash) {
  74. $length = length($entry) if (length($entry) > $length);
  75. }
  76. return $length;
  77. }
  78. sub ConvertTokens($) {
  79. my %tokens_h;
  80. # unique
  81. foreach my $token (split(" ", $_[0])) {
  82. $tokens_h{$token} = 1;
  83. }
  84. # map tokens
  85. my @result_a;
  86. if (defined $tokens_h{"sync-token"}) {
  87. push @result_a, "ST";
  88. }
  89. if (defined $tokens_h{"sync-collection"}) {
  90. push @result_a, "SC";
  91. }
  92. if (defined $tokens_h{"getctag"}) {
  93. push @result_a, "GCT";
  94. }
  95. if (defined $tokens_h{"getetag"}) {
  96. push @result_a, "GET";
  97. }
  98. # TODO: add potential others which causing long duration
  99. $result = "";
  100. if (scalar(@result_a) > 0) {
  101. $result = ":F=" . join(",", @result_a);
  102. }
  103. return $result;
  104. }
  105. sub ConvertLoc($) {
  106. my $loc = $_[0];
  107. if (defined $Locations{$loc}) {
  108. # from cache
  109. return ":L=" . $Locations{$loc};
  110. } elsif (defined $LocationsFile{$loc}) {
  111. # from cache
  112. return ":L=" . $LocationsFile{$loc};
  113. }
  114. if ($loc =~ /\/'$/o) {
  115. $Locations{$loc} = "L=" . substr(Digest::SHA::sha256_hex($loc), 0, 8);
  116. return ":" . $Locations{$loc};
  117. } else {
  118. $LocationsFile{$loc} = "L=<FILE>";
  119. return ":" . $Locations{$loc};
  120. }
  121. }
  122. sub ConvertLogin($) {
  123. my $login = $_[0];
  124. if (defined $LoginsHash{$loginc}) {
  125. # from cache
  126. return $LoginsHash{$login};
  127. }
  128. $LoginsHash{$login} = "U=" . substr(Digest::SHA::sha256_hex($login), 0, 8);
  129. return $LoginsHash{$login};
  130. }
  131. while (defined($ThisLine = <STDIN>)) {
  132. # count loglevel
  133. if ( $ThisLine =~ /\[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\] /o ) {
  134. $Loglevel{$1}++
  135. }
  136. # parse log for events
  137. if ( $ThisLine =~ /Radicale server ready/o ) {
  138. $OtherEvents{"Radicale server started"}++;
  139. }
  140. elsif ( $ThisLine =~ /Stopping Radicale/o ) {
  141. $OtherEvents{"Radicale server stopped"}++;
  142. }
  143. elsif ( $ThisLine =~ / (\S+) response status/o ) {
  144. my $req = $1;
  145. if ( $ThisLine =~ / \S+ response status for (.*) with depth '(\d)' in ([0-9.]+) seconds: (\d+)/o ) {
  146. $req .= ":D=" . $2 . ":R=" . $4;
  147. $req .= ConvertLoc($1) if ($Detail >= 20);
  148. ResponseTimesMinMaxSum($req, $2) if ($Detail >= 10);
  149. } elsif ( $ThisLine =~ / \S+ response status for (.*) in ([0-9.]+) seconds: (\d+)/o ) {
  150. $req .= ":R=" . $3;
  151. $req .= ConvertLoc($1) if ($Detail >= 20);
  152. ResponseTimesMinMaxSum($req, $1) if ($Detail >= 10);
  153. } elsif ( $ThisLine =~ / \S+ response status for (.*) with depth '(\d)' in ([0-9.]+) seconds (\S+) (\d+) bytes: (\d+)/o ) {
  154. $req .= ":D=" . $2 . ":R=" . $6;
  155. $req .= ConvertLoc($1) if ($Detail >= 20);
  156. ResponseTimesMinMaxSum($req, $3) if ($Detail >= 10);
  157. ResponseSizesMinMaxSum($req, $4, $5) if ($Detail >= 10);
  158. } elsif ( $ThisLine =~ / \S+ response status for (.*) in ([0-9.]+) seconds (\S+) (\d+) bytes: (\d+)/o ) {
  159. $req .= ":R=" . $5;
  160. $req .= ConvertLoc($1) if ($Detail >= 20);
  161. ResponseTimesMinMaxSum($req, $2) if ($Detail >= 10);
  162. ResponseSizesMinMaxSum($req, $3, $4) if ($Detail >= 10);
  163. } elsif ( $ThisLine =~ / \S+ response status for (.*) with depth '(\d)' in ([0-9.]+) seconds (\S+) (\d+) bytes \((.*)\): (\d+)/o ) {
  164. $req .= ":D=" . $2 . ":R=" . $7;
  165. $req .= ConvertLoc($1) if ($Detail >= 20);
  166. $req .= ConvertTokens($6) if ($Detail >= 15);
  167. ResponseTimesMinMaxSum($req, $3) if ($Detail >= 10);
  168. ResponseSizesMinMaxSum($req, $4, $5) if ($Detail >= 10);
  169. } elsif ( $ThisLine =~ / \S+ response status for (.*) in ([0-9.]+) seconds (\S+) (\d+) bytes \((.*)\): (\d+)/o ) {
  170. $req .= ":R=" . $6;
  171. $req .= ConvertLoc($1) if ($Detail >= 20);
  172. $req .= ConvertTokens($6) if ($Detail >= 15);
  173. ResponseTimesMinMaxSum($req, $2) if ($Detail >= 10);
  174. ResponseSizesMinMaxSum($req, $3, $4) if ($Detail >= 10);
  175. }
  176. $Responses{$req}++;
  177. }
  178. elsif ( $ThisLine =~ / (\S+) request for ('[^']+')/o ) {
  179. my $req = $1;
  180. my $loc = $2;
  181. if ( $ThisLine =~ / with depth '(\d)' received/o ) {
  182. $req .= ":D=" . $1;
  183. }
  184. $req .= ConvertLoc($loc) if ($Detail >= 20);
  185. $Requests{$req}++;
  186. if ( $ThisLine =~ /using ('.*')/o ) {
  187. my $ua = $1;
  188. # remove unexpected chars
  189. $ua =~ s/[\x00-\x1F\x7F-\xFF]//g;
  190. $ua .= ConvertLoc($loc) if ($Detail >= 20);
  191. $UserAgents{$ua}++ if ($Detail >= 18);
  192. }
  193. }
  194. elsif ( $ThisLine =~ / (Successful login): '([^']+)'/o ) {
  195. my $login = $2;
  196. $login = ConvertLogin($login) if ($Detail >= 20);
  197. $Logins{$login}++ if ($Detail >= 5);
  198. $OtherEvents{$1}++;
  199. }
  200. elsif ( $ThisLine =~ / (Failed login attempt) /o ) {
  201. $OtherEvents{$1}++;
  202. }
  203. elsif ( $ThisLine =~ / (Profiling data per request method \S+) /o ) {
  204. my $info = $1;
  205. if ( $ThisLine =~ /(no request seen so far)/o ) {
  206. $OtherEvents{$info . " - " . $1}++;
  207. } else {
  208. $OtherEvents{$info}++;
  209. };
  210. }
  211. elsif ( $ThisLine =~ / (Profiling data per request \S+) /o ) {
  212. my $info = $1;
  213. if ( $ThisLine =~ /(suppressed because duration below minimum|suppressed because of no data)/o ) {
  214. $OtherEvents{$info . " - " . $1}++;
  215. } else {
  216. $OtherEvents{$info}++;
  217. };
  218. }
  219. elsif ( $ThisLine =~ /\[(DEBUG|INFO)\] /o ) {
  220. # skip if DEBUG+INFO
  221. }
  222. else {
  223. # Report any unmatched entries...
  224. if ($ThisLine =~ /^({\'| )/o) {
  225. # skip profiling or raw header data
  226. next;
  227. };
  228. if ($ThisLine =~ /^$/o) {
  229. # skip empty line
  230. next;
  231. };
  232. $ThisLine =~ s/^\[\d+(\/Thread-\d+)?\] //; # remove process/Thread ID
  233. chomp($ThisLine);
  234. $OtherList{$ThisLine}++;
  235. }
  236. }
  237. if ($Started) {
  238. print "\nStatistics:\n";
  239. print " Radicale started: $Started Time(s)\n";
  240. }
  241. if (keys %Loglevel) {
  242. $sum = Sum(\%Loglevel);
  243. print "\n**Loglevel counters**\n";
  244. printf "%-18s | %7s | %9s |\n", "Loglevel", "cnt", "ratio";
  245. print "-" x42 . "\n";
  246. foreach my $level (sort keys %Loglevel) {
  247. printf "%-18s | %7d | %7.3f%% |\n", $level, $Loglevel{$level}, (($Loglevel{$level} * 100) / $sum);
  248. }
  249. print "-" x42 . "\n";
  250. printf "%-18s | %7d | %7.3f%% |\n", "", $sum, 100;
  251. }
  252. if (keys %Logins) {
  253. $sum = Sum(\%Logins);
  254. $length = MaxLength(\%Logins);
  255. print "\n**Successful login counters**\n";
  256. printf "%-" . $length . "s | %7s | %9s |\n", "Login", "cnt", "ratio";
  257. print "-" x($length + 24) . "\n";
  258. foreach my $login (sort keys %Logins) {
  259. printf "%-" . $length . "s | %7d | %7.3f%% |\n", $login, $Logins{$login}, (($Logins{$login} * 100) / $sum);
  260. }
  261. print "-" x($length + 24) . "\n";
  262. printf "%-" . $length . "s | %7d | %7.3d%% |\n", "", $sum, 100;
  263. }
  264. if (keys %UserAgents) {
  265. $sum = Sum(\%UserAgents);
  266. $length = MaxLength(\%UserAgents);
  267. print "\n**UserAgent Counters**\n";
  268. print "* Location: L=<HASH> -> see below L=<FILE> -> see raw log\n" if (scalar(keys %Locations) > 0);
  269. printf "%-" . $length . "s | %7s | %9s |\n", "UserAgent", "cnt", "ratio";
  270. print "-" x($length + 24) . "\n";
  271. foreach my $ua (sort keys %UserAgents) {
  272. printf "%-" . $length . "s | %7d | %7.3f%% |\n", $ua, $UserAgents{$ua}, (($UserAgents{$ua} * 100) / $sum);
  273. }
  274. print "-" x($length + 24) . "\n";
  275. printf "%-" . $length . "s | %7d | %7.3d%% |\n", "", $sum, 100;
  276. }
  277. if (keys %Requests) {
  278. $sum = Sum(\%Requests);
  279. $length = MaxLength(\%Requests);
  280. print "\n**Request counters (D=<depth>)**\n";
  281. print "* Location: L=<HASH> -> see below L=<FILE> -> see raw log\n" if (scalar(keys %Locations) > 0);
  282. printf "%-" . $length . "s | %7s | %9s |\n", "Request", "cnt", "ratio";
  283. print "-" x($length + 24) . "\n";
  284. foreach my $req (sort keys %Requests) {
  285. printf "%-" . $length . "s | %7d | %7.3f%% |\n", $req, $Requests{$req}, (($Requests{$req} * 100) / $sum);
  286. }
  287. print "-" x($length + 24) . "\n";
  288. printf "%-18s | %7d | %7.3f%% |\n", "", $sum, 100;
  289. }
  290. if (keys %Responses) {
  291. $sum = Sum(\%Responses);
  292. $length = MaxLength(\%Responses);
  293. print "\n**Response result counters ((D=<depth> R=<result>)**\n";
  294. print "* Flags: ST:sync-token SC:sync-collection GCT:getctag GET:getetag\n" if ($Detail >= 15);
  295. print "* Location: L=<HASH> -> see below L=<FILE> -> see raw log\n" if (scalar(keys %Locations) > 0);
  296. printf "%-" . $length . "s | %7s | %9s |\n", "Response", "cnt", "ratio";
  297. print "-" x($length + 24) . "\n";
  298. foreach my $req (sort keys %Responses) {
  299. printf "%-" . $length . "s | %7d | %7.3f%% |\n", $req, $Responses{$req}, (($Responses{$req} * 100) / $sum);
  300. }
  301. print "-" x($length + 24) . "\n";
  302. printf "%-" . $length . "s | %7d | %7.3f%% |\n", "", $sum, 100;
  303. }
  304. if (keys %ResponseTimes) {
  305. $length = MaxLength(\%ResponseTimes);
  306. print "\n**Response timings (counts, seconds) (D=<depth> R=<result> F=<flags>)**\n";
  307. print "* Flags: ST:sync-token SC:sync-collection GCT:getctag GET:getetag\n" if ($Detail >= 15);
  308. print "* Location: L=<HASH> -> see below L=<FILE> -> see raw log\n" if (scalar(keys %Locations) > 0);
  309. printf "%-" . $length . "s | %7s | %7s | %7s | %7s |\n", "Response", "cnt", "min", "max", "avg";
  310. print "-" x($length + 42) . "\n";
  311. foreach my $req (sort keys %ResponseTimes) {
  312. printf "%-" . $length . "s | %7d | %7.3f | %7.3f | %7.3f |\n", $req
  313. , $ResponseTimes{$req}->{'cnt'}
  314. , $ResponseTimes{$req}->{'min'}
  315. , $ResponseTimes{$req}->{'max'}
  316. , $ResponseTimes{$req}->{'sum'} / $ResponseTimes{$req}->{'cnt'};
  317. }
  318. print "-" x($length + 42) . "\n";
  319. }
  320. if (keys %ResponseSizes) {
  321. for my $type (sort keys %ResponseSizes) {
  322. $length = MaxLength($ResponseSizes{$type});
  323. print "\n**Response sizes (counts, bytes: $type) (D=<depth> R=<result>)**\n";
  324. print "* Flags: ST:sync-token SC:sync-collection GCT:getctag GET:getetag\n" if ($Detail >= 15);
  325. print "* Location: L=<HASH> -> see below L=<FILE> -> see raw log\n" if (scalar(keys %Locations) > 0);
  326. printf "%-" . $length . "s | %7s | %9s | %9s | %9s |\n", "Response", "cnt", "min", "max", "avg";
  327. print "-" x($length + 48) . "\n";
  328. foreach my $req (sort keys %{$ResponseSizes{$type}}) {
  329. printf "%-" . $length . "s | %7d | %9d | %9d | %9d |\n", $req
  330. , $ResponseSizes{$type}->{$req}->{'cnt'}
  331. , $ResponseSizes{$type}->{$req}->{'min'}
  332. , $ResponseSizes{$type}->{$req}->{'max'}
  333. , $ResponseSizes{$type}->{$req}->{'sum'} / $ResponseSizes{$type}->{$req}->{'cnt'};
  334. }
  335. print "-" x($length + 48) . "\n";
  336. }
  337. }
  338. if (keys %OtherEvents) {
  339. print "\n**Other Events**\n";
  340. foreach $ThisOne (sort keys %OtherEvents) {
  341. print "$ThisOne: $OtherEvents{$ThisOne} Time(s)\n";
  342. }
  343. }
  344. if (keys %OtherList) {
  345. print "\n**Unmatched Entries**\n";
  346. foreach $ThisOne (sort keys %OtherList) {
  347. print "$ThisOne: $OtherList{$ThisOne} Time(s)\n";
  348. }
  349. }
  350. if (scalar(keys %LoginsHash) > 0) {
  351. print "\n**Map of login hashes (REMOVE THIS FOR PRIVACY REASONS before submit)**\n";
  352. $length = MaxLength(\%LoginsHash);
  353. printf "%-10s | %-" . $length . "s | \n", "Hash", "Login";
  354. print "-" x($length + 15) . "\n";
  355. foreach my $login (sort { $LoginsHash{$a} cmp $LoginsHash{$b} } keys %LoginsHash) {
  356. printf "%10s | %-" . $length . "s |\n", $LoginsHash{$login}, $login;
  357. }
  358. print "-" x($length + 15) . "\n";
  359. }
  360. if (scalar(keys %Locations) > 0) {
  361. print "\n**Map of location hashes (REMOVE THIS FOR PRIVACY REASONS before submit)**\n";
  362. $length = MaxLength(\%Locations);
  363. printf "%-10s | %-" . $length . "s | \n", "Hash", "Location";
  364. print "-" x($length + 15) . "\n";
  365. foreach my $loc (sort { $Locations{$a} cmp $Locations{$b} } keys %Locations) {
  366. printf "%10s | %-" . $length . "s |\n", $Locations{$loc}, $loc;
  367. }
  368. print "-" x($length + 15) . "\n";
  369. }
  370. exit(0);
  371. # vim: shiftwidth=3 tabstop=3 syntax=perl et smartindent