├── .editorconfig ├── .gitignore ├── AUTHORS ├── COPYING ├── ChangeLog ├── INSTALL ├── LICENSE ├── Makefile.am ├── NEWS ├── README ├── autogen.sh ├── configure.ac ├── man ├── Makefile.am └── apachetop.1 └── src ├── Makefile.am ├── apachetop.cc ├── apachetop.h ├── circle.h ├── display.cc ├── display.h ├── filters.cc ├── filters.h ├── hits_circle.cc ├── hits_circle.h ├── inlines.cc ├── log.cc ├── log.h ├── map.cc ├── map.h ├── ohtbl.cc ├── ohtbl.h ├── pcre2_cpp_wrapper.h ├── queue.cc ├── queue.h ├── resolver.cc ├── resolver.h ├── timed_circle.cc └── timed_circle.h /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | 8 | [*.{cc,c,h}] 9 | indent_style = tab 10 | indent_size = 4 11 | tab_width = 4 12 | 13 | [{Makefile,Makefile.am}] 14 | indent_style = tab 15 | 16 | [configure.ac] 17 | indent_style = space 18 | indent_size = 4 19 | 20 | [.travis.yml] 21 | indent_style = space 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *Makefile 2 | *.o 3 | src/.deps/ 4 | stamp-h1 5 | *~ 6 | autom4te.cache/ 7 | src/apachetop 8 | config.h 9 | config.log 10 | config.status 11 | *.in 12 | configure 13 | aclocal.m4 14 | config/ 15 | config.nice 16 | *.tar.gz* 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Chris Elsworth 2 | Alex Kovalyov 3 | Nickolas Toursky 4 | Helmut K. C. Tessarek 5 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | SEE LICENSE 2 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | ApacheTop ChangeLog 2 | 3 | v0.23.2 (15th February, 2023) 4 | * switch to PCRE2, --with-pcre2= 5 | * fix for c++17 (@thesamesam) 6 | * fix a potential seagfault, if regex is not valid 7 | * if regex is not valid, treat the input as literal string 8 | 9 | 10 | v0.19.7 (20th July, 2019) 11 | * attempt to find ncurses via pkg-config first (@Polynomial-C) 12 | * add .editorconfig for consistent coding style 13 | 14 | 15 | v0.18.4 (12th April, 2018) 16 | * fix for clang6 (@gonzalo) 17 | * add better build instructions to INSTALL 18 | * add option (-v) to show version 19 | * fix minor mem leak (8 bytes per apachetop invocation) 20 | 21 | 22 | v0.17.4 (25th April, 2017) 23 | * create config.nice when running ./configure 24 | * fix error: extra qualification on member (when using adns) 25 | * fix compiler warnings 26 | * fix a potential buffer overflow 27 | 28 | 29 | v0.15.6 (10th June, 2015) 30 | * allow other time/date formats in Apache log 31 | * use sys/param.h for MAXPATHLEN 32 | * fix deprecated auto tools macros 33 | * fix compiler warnings 34 | * use silent compile rules 35 | * updated man page 36 | * change version to 0.YY.M of new release 37 | 38 | 39 | v0.12.6 (27th October, 2005) 40 | * fixed security issue which described at CVE-2005-2660 41 | 42 | 43 | v0.12.5 (27th November, 2004) 44 | 20041126 45 | * Threading is no longer used. Everything now runs in a single thread. 46 | * change src/Makefile.am to install apachetop into bin, not sbin 47 | * add NetBSD compatibility hack for kqueue; their opaque datafield 48 | is an intptr_t (why?) 49 | 50 | 20040915 51 | * add attron back into configure.ac - not sure when that got removed 52 | * change timeout delay between log checks to a constant 1/10th of a second 53 | * get rid of all threading code. It was never really necessary; only ever 54 | implemented because the main-loop delay was too long. Now it runs at 55 | least 10 times a second. 56 | 57 | 20040725 58 | * incorporate gcc 3.4 building compatibility patch 59 | from Pascal Terjan 60 | 61 | 62 | v0.12 (21st May, 2004) 63 | * Add FAM support; event notification for Linux etc. 64 | * Add adns support; ApacheTop can now resolve your IPs into Hosts. 65 | Please give me feedback on this; it's not very widely tested. 66 | You'll need to use the cmdline switch -r to enable it. 67 | * Add return code breakdown for each item. Press 'n' while running. 68 | * Header now converts to GB/MB/KB/Bytes where appropriate. 69 | * TAKE NOTE! Commandline option changes: 70 | -r for refresh delay has changed to -d 71 | -r has been re-assigned to enable resolving of hosts/ips. 72 | 73 | 20040508 74 | * finish up adns resolving; now host and/or ip are displayed as best 75 | as possible; if one is not available it is displayed as ... while 76 | being resolved. 77 | * TAKE NOTE! 78 | commandline option -r has changed to -d 79 | -r has been reassigned to enable resolving of hosts/ips 80 | 81 | 20040505 82 | * use kqueue and fam facilities for passing opaque user data back when 83 | an event occurs, allowing the removal of the ift struct (which sucked) 84 | 85 | 20040504 86 | * add runtime option key 'n' to toggle number columns between hits/bytes 87 | and return code breakdowns for each item. 88 | * expand sorting options so it's possible to sort by return codes as well 89 | as hits/bytes. Pressing 's' brings up a different menu if you're viewing 90 | return code breakdown. 91 | 92 | 20040416 93 | * add return code breakdown for each item in the top-list 94 | 95 | 20040308 96 | * improve header to display GB/MB/KB/Bytes in all appropriate fields 97 | 98 | 20040303 99 | * add adns support to resolve IPs into Hosts; if HostnameLookups is Off in 100 | your httpd.conf, ApacheTop will now look up IPs for you. Created 101 | --with-adns, like pcre/fam. 102 | 103 | 20040301 104 | * remove --enable-pcre; have configure always check for it and just warn if 105 | it can't be found. Specify --with-pcre to point it at the right place. 106 | * call realpath() on supplied filenames for sanity 107 | * add File Alteration Module (FAM) monitoring code (for Linux/IRIX mostly) 108 | Use --with-fam to point it at the right place, like pcre. 109 | 110 | 111 | v0.11 (25th February, 2004) 112 | 20040224 113 | * acknowledge & as starting a query string as well as ? 114 | 115 | 20040219 116 | * clean up configure.ac, remove unused function checks 117 | * add --with-libraries and --with-includes to ./configure to provide 118 | hints about where to look other than standard places 119 | 120 | 20040218 121 | * add --enable-pcre and --with-pcre= to ./configure; providing 122 | these enables regular expression filtering 123 | * add new filters submenu (press f) 124 | * add --with-logfile= to ./configure; now you can override the 125 | default log position without editing the source 126 | 127 | 20040216 128 | * inline hashing functions for greater efficiency 129 | * replace localtime & strftime calls with a bit of maths in display_header() 130 | 131 | 20040204 132 | * add largefile checks into configure.ac 133 | 134 | 20040119 135 | * change nanosleep to usleep; I think this makes more sense.. 136 | Solaris 2.6 doesn't have nanosleep, and it seems overkill 137 | * change configure.ac check for wattr_on to attron (fix Solaris compile issue) 138 | 139 | 140 | v0.10 (14th January, 2004) 141 | 20040104 142 | * remove old useless configure.ac stuff 143 | * fix crash bug when ApacheTop has nothing to display 144 | 145 | 146 | v0.9 (22nd December, 2003) 147 | 20031222 148 | * don't call endwin() in a signal handler; set a flag and get the main 149 | loop to do it. Fixes intermittent segfault when Ctrl-C'ing to exit. 150 | * change instances of mvprintw() to mvaddstr() where printf features 151 | were unused anyway; should give curses an easier time. 152 | * add runtime help display; press s or ? 153 | 154 | 20031221 155 | * rework runtime options to make use of "submenus" 156 | * add facility for removing detailed-display sections (remove Referrers 157 | from a URL detailed display etc); press t during runtime for submenu 158 | * move sort runtime keys into their own submenu; press s for this 159 | 160 | 20031218 161 | * revamp pthreads detection in configure.ac. It works now, I swear. 162 | 163 | 20031212 164 | * code cleanups in display.cc 165 | * new display mode; press Right-Arrow to show statistics specific to the 166 | currently highlighted item. If you are highlighting a URL, host and 167 | referrer statistics for that URL will be shown. Press Left-Arrow to exit 168 | this display mode. 169 | 170 | 171 | v0.8.1 (28th November, 2003) 172 | 20031128 173 | * fix for running ApacheTop with no parameters 174 | 175 | 176 | v0.8 (18th November, 2003) 177 | 20031115 178 | * we now reopen an input file if the inode changes; this catches deletes 179 | and renames. ApacheTop will wait for the file to be recreated and reopen. 180 | 181 | 20031113 182 | * remove mod_log_spread code; I'm not happy with including this since I 183 | have no idea how it works, no idea how the new filecode breaks it, and 184 | I haven't had chance to test it. This will be re-introduced when I can 185 | test it 186 | * remove option -t; all logs are assumed to be common or combined format 187 | * adapt internally to facilitate reading from more than one log at once 188 | 189 | 20031102 190 | * add more kqueue flags to watch for file deletion/renaming 191 | 192 | 20031027 193 | * fiddle with configure.ac so it doesn't require autoconf 2.57 194 | 195 | 20031014 196 | * added check for stdarg.h to configure.ac; fixes Solaris build issue 197 | * fix configure.ac check for pthread library 198 | 199 | 200 | v0.7 (14th October, 2003) 201 | 20031008 202 | * add thread for monitoring keypresses; if pthreads can be used, this will 203 | make ApacheTop feel more responsive. Fall back on old behaviour if not. 204 | 205 | 20031006 206 | * simplify log-fetching routine; no need to lseek() around the file; 207 | we just wait (or kqueue) then attempt to read(). 208 | 209 | 20031005 210 | * wrap header includes in #if/#endif; we should now build on ancient 211 | standards-breaking systems which don't have some headers. 212 | * efficiency tweaks to hashing functions and hash class 213 | * use the return value of ohtbl->insert where possible; saves a call to 214 | lookup and thus another hash in some cases, which cuts down on CPU time. 215 | 216 | 217 | v0.6 (5th October, 2003) 218 | 20031004 219 | * change from select() to nanosleep() for sleeping. Not only is this a more 220 | suitable function, it also stops ApacheTop on Linux eating all the CPU, 221 | since select() was zero'ing a structure after it's first call. doh. 222 | * start distributing with autoconf. Disregard previous build instructions; 223 | you now do the more conventional cd apachetop-X.X/ && ./configure && make 224 | * add kqueue() support for checking the logfile for updates; autoconf 225 | enables this automatically if your architecture supports it. 226 | 227 | 20031003 228 | * drop qsort() in favour of shellsort() in display.cc; this has the 229 | advantage of knowing the data it's sorting, thus doesn't have to call a 230 | comparison sub-function millions of times. This should lower ApacheTop's 231 | CPU usage a lot. 232 | * add mod_log_spread support via a patch from Theo Schlossnagle; I haven't 233 | been able to test this yet. 234 | 235 | 236 | v0.5 (2nd October, 2003) 237 | 20031002 238 | * make the timed circle store slightly more information than requested, so 239 | that we're guaranteed to have the requested number of seconds. 240 | 241 | 20031001 242 | * add option -r to specify refresh delay interval in seconds. 243 | * add runtime key 'p' to pause/unpause the updating display. Stats are 244 | still collected, but the screen is temporarily frozen. 245 | 246 | 20030930 247 | * experimental try-to-stop-it-crashing changes; a few cases of possible 248 | memory trashing were fixed in hash and map classes. 249 | * fill in dummy "Unknown" value for referrer if a common log is used. 250 | (fixes crash if user switches to REFERRERs when there aren't any) 251 | * change default refresh delay to 5 seconds. 252 | 253 | 254 | v0.4 (29th September, 2003) 255 | 20030929 256 | * add a new display mode; REFERRERs. 257 | Press 'd' to cycle through URLs, IPs, and now REFERRERs. 258 | * add option -p to preserve http:// (or whatever protocol) at start of 259 | referrer string. By default it is cut out to save space onscreen. 260 | 261 | 20030928 262 | * remove -lreadline and associated code; hence dropping support for 263 | altering delay frequency during runtime. This improves portability, 264 | notably to Solaris. Intending to replace readline with own code. 265 | * -L changed to -l; no real reason to use uppercase, not sure why I did. 266 | 267 | 20030927 268 | * add -s option to keep a given number of URL path segments only. For 269 | example, -s 2 converts "/images/media/small/x.jpg" into "/images/media/" 270 | This is rather experimental, so I'd appreciate feedback. 271 | * remove floating points from table information when the numbers get big 272 | enough; so we can display bigger numbers in the same table space. 273 | * internally record referrers from log if they're available. 274 | * add a visual marker (* just to left of URL/IP), movable by Up/Dn to stats 275 | table; will be used to provide more information on the selected item. 276 | This doesn't yet do anything further, however. 277 | 278 | 279 | v0.3 (26th September, 2003) 280 | 20030926 281 | * add framework for reading more logformats; combined & atop (not done yet) 282 | * add -L option to lowercase all URLs for display (means /FOO and /foo are 283 | considered the same and accumulate the same hits/bytes counters) 284 | * add -q option to keep querystrings on URLs (default is to remove) 285 | * rejigged -h and -t options to be -H and -T; logtype now uses -t 286 | 287 | 20030925 288 | * simplify circle functions by passing structs around, not lots of 289 | individual ints; cuts down on memory copying too (= faster). 290 | * display bytecount in header in MB when appropriate. 291 | * adapt to resizes of the term; display more results if we can, and use 292 | more of the term's width if there's an excess. 293 | 294 | 20030924 295 | * log.[cc|h] added; Log parsing class. Obsoletes logsplit() in apachetop.cc. 296 | Currently only has common logformat parser, but is easily extendable to do 297 | combined (and planning on a custom format) 298 | * don't display NaN when there's no stats; display zeroes. 299 | 300 | 301 | v0.2 (24th September, 2003) 302 | 20030923 303 | * timed_circle.cc operational improvements to return more accurate 304 | information about what's really going on with idle-ish servers. 305 | * only allow one of -t and -h args to be specified. 306 | 307 | 20030922 308 | * timed_circle.cc fixes for potentially overrunning arrays, with the help 309 | of Purify on Solaris. This should fix the segmentation faults a few people 310 | have been randomly seeing. 311 | 312 | 313 | v0.1 (22nd September, 2003) 314 | 20030922 315 | * README (some brief docs) and CHANGES added :) 316 | * display restructured a little; better use made of floating points 317 | 318 | 20030921 319 | * circle.[cc|h] moved to hits_circle.*; circle.h added as a virtual class. 320 | In English, this means you can choose which circle mode to use at startup 321 | time; one limited by time or one by hits. 322 | 323 | 20030920 324 | * timed_circle.cc added; provide detailed statistics for server requests in 325 | the last $x seconds. The alternative is circle.cc, which similarly 326 | remembers the last $x requests, regardless of age. 327 | 328 | 329 | v0.0 (19th September, 2003) 330 | 20030919 331 | * initial freshmeat release 332 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | +----------------------------------------------------------------------+ 2 | | ApacheTop INSTALL Instructions | 3 | +----------------------------------------------------------------------+ 4 | 5 | ApacheTop is now distributed with autoconf files, a nice easy way to build 6 | distributions whatever platform you're on. 7 | 8 | 1) Building ApacheTop from a cloned repository 9 | 2) Building ApacheTop from a tarball 10 | 3) custom configure options 11 | 4) If it fails to build 12 | 13 | +----------------------------------------------------------------------+ 14 | | 1. Building ApacheTop from a cloned repository | 15 | +----------------------------------------------------------------------+ 16 | 17 | git clone https://github.com/tessus/apachetop.git 18 | 19 | cd apachetop 20 | ./autogen.sh (autotools required) 21 | ./configure (see section 3 for details) 22 | make 23 | make install (as root or with sudo) 24 | 25 | +----------------------------------------------------------------------+ 26 | | 2. Building ApacheTop from a tarball | 27 | +----------------------------------------------------------------------+ 28 | 29 | Download the latest tarball from: 30 | https://github.com/tessus/apachetop/releases/latest 31 | 32 | tar -xzf apachetop-X.Y.Z.tar.gz 33 | 34 | cd apachetop-X.Y.Z 35 | ./configure (see section 3 for details) 36 | make 37 | make install (as root or with sudo) 38 | 39 | +----------------------------------------------------------------------+ 40 | | 3. custom configure options | 41 | +----------------------------------------------------------------------+ 42 | 43 | There's a few custom ./configure options and overrides: 44 | 45 | --with-logfile= (added in v0.11) 46 | You may specify the location of the default logfile 47 | to open. This overrides the #define in apachetop.h. 48 | Of course, you can just use -f on the apachetop 49 | commandline, but if you'll only ever use one log, you 50 | can give it to configure and never worry about it again. 51 | 52 | --with-pcre2= (added in v0.23.2) 53 | Specifies where to find the pcre2 installation in the 54 | event it's not in your standard path. configure should look 55 | for /include/pcre2.h and /lib/libpcre2.* 56 | 57 | --with-fam= (added in v0.12) 58 | Specifies where to find the FAM installation. The path you 59 | give should contain include/fam.h and lib/libfam.* 60 | kqueue will be preferred to fam if both are found on the system. 61 | 62 | --with-pcre= (added in v0.12) 63 | Specifies where to find an adns installation. 64 | 65 | 66 | +----------------------------------------------------------------------+ 67 | | 4. If it fails to build | 68 | +----------------------------------------------------------------------+ 69 | 70 | Make sure you have the -dev sources for readline and (n)curses. These 71 | contain files required to build binaries that use these libraries; for 72 | example on Debian you'll need the libreadline4-dev package installed. 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License: BSD 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in 11 | the documentation and/or other materials provided with the 12 | distribution. 13 | 3. The names of the authors may not be used to endorse or promote 14 | products derived from this software without specific prior 15 | written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR 18 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. 20 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | ## Process this file with automake to produce Makefile.in 2 | 3 | AUTOMAKE_OPTIONS = foreign 4 | SUBDIRS = man src 5 | 6 | EXTRA_DIST = LICENSE INSTALL README ChangeLog 7 | 8 | MAINTAINERCLEANFILES = Makefile.in aclocal.m4 configure configuration.h.in \ 9 | stamp-h.in 10 | 11 | dist-sign: 12 | gpg -ba $(PACKAGE_NAME)-$(PACKAGE_VERSION).tar.gz 13 | 14 | .PHONY: dist-all 15 | dist-all: dist dist-sign 16 | 17 | dist-all-clean: 18 | rm -f $(PACKAGE_NAME)-$(PACKAGE_VERSION).tar.gz 19 | rm -f $(PACKAGE_NAME)-$(PACKAGE_VERSION).tar.gz.asc 20 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | see ChangeLog 2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ApacheTop Readme 2 | 3 | ApacheTop watches a logfile generated by Apache (in standard common or 4 | combined logformat, and generates human-parsable output in realtime. 5 | 6 | See the INSTALL file for ./configure options (there's a few newly added 7 | since v0.11) 8 | 9 | Several commandline options dictate some of its' behaviour: 10 | -f logfile 11 | Select which file to watch. 12 | Specify this option multiple times to watch multiple files 13 | 14 | -H hits | -T time 15 | These options are mutually exclusive. Specify only one, if any at 16 | all. They work as follows. ApacheTop maintains a table of 17 | information internally containing all the relevant information about 18 | the hits it's seen. This table can only be a finite size, so you 19 | need to decide how big it's going to be. You have two options. 20 | You can either: 21 | Use -H to say "remember hits" 22 | or Use -T to say "remember all hits in seconds" 23 | 24 | The default (at the moment) is to remember hits for 30 seconds. 25 | Setting this too large (whichever option you choose) will cause 26 | ApacheTop to use more memory and more CPU time. My experimentation 27 | finds that remembering no more than around 5000 requests works well. 28 | 29 | -q 30 | Instructs ApacheTop to keep the querystrings, not remove them 31 | 32 | -l 33 | Instructs ApacheTop to lowercase all URLs, thus /FOO and /foo are 34 | treated as the same and accumulate the same statistics. 35 | 36 | -r 37 | Enable resolving of hosts/ips (you need adns!) 38 | 39 | -s segments 40 | Instructs ApacheTop to only keep the first parts of the 41 | path. Trailing slashes are kept if present. Statistics are then 42 | merged for each truncated url. 43 | This is easiest to demonstrate with examples: 44 | -s 2 would produce the following: 45 | /media/x.jpg -> /media/x.jpg 46 | /media/images/x.jpg -> /media/images/ 47 | /media/images/small/x.jpg -> /media/images/ 48 | /media/images/big/x.jpg -> /media/images/ 49 | Stats for the last three URLs would be merged in this case. 50 | 51 | -p 52 | Instructs ApacheTop to keep the protocol (http:// usually) at the 53 | front of its' referrer strings. Normal behaviour is to remove them 54 | to give more room to more useful information. 55 | 56 | 57 | -d secs 58 | Set default refresh delay, in seconds. 59 | 60 | 61 | Once it's running, you'll see a display like this: 62 | 63 | last hit: 09:17:07 atop runtime: 0 days, 00:58:20 09:17:08 64 | All: 638924 reqs ( 182.65/sec) 3433539K ( 981.6K/sec) ( 5.4K/req) 65 | 2xx: 455415 (71.3%) 3xx: 175745 (27.5%) 4xx: 7746 ( 1.2%) 5xx: 10 ( 0.0%) 66 | R ( 30s): 5195 reqs ( 173.17/sec) 25405K ( 846.8K/sec) ( 4.9K/req) 67 | 2xx: 3447 (66.4%) 3xx: 1715 (33.0%) 4xx: 33 ( 0.6%) 5xx: 0 ( 0.0%) 68 | 69 | REQS REQ/S KB KB/S URL 70 | 103 3.4 2983 99.4 / 71 | 56 1.9 239 8.0 /tickerdata/story2.dat 72 | 47 1.6 104 3.6 /home/today/patina.js 73 | 44 1.5 82 2.8 /home/styles/home_d0e2ee.css 74 | 75 | 76 | 77 | The top line displays the time the last hit was seen, how long it's been 78 | running, and the current time. 79 | 80 | The next two lines display information about every single hit ApacheTop has 81 | processed in this incarnation. 82 | Firstly you see how many hits the data is representing. After that, the 83 | average number of hits/second since starting. Following that, the total number 84 | of KB witnessed; then the average KB/sec. Finally you see the average KB per 85 | request. 86 | The next line shows a breakdown of return codes; in this particular example you 87 | can see that 71.3% of the hits returned a 2xx code. 27.5% were 3xx, and so on. 88 | You also have the actual number of hits in each group. 89 | 90 | The two lines below this are where the commandline options -h and -t come in. 91 | The data in these lines reads the same as the two above them, but this data is 92 | only for the hits remembered in ApacheTop's internal table (remember that?). 93 | You can see how many seconds of data this represents in the R ( 30s) at the 94 | beginning of the line. This is for 30 seconds. These two lines of information 95 | are good for a "what is my server doing *right now*?" scenario, while the two 96 | above them are good for a picture over the course of a few minutes or hours. 97 | 98 | Underneath this header, you'll see a list of URLs along with their relevant 99 | number of requests, requests per second, kb, and kb per second. 100 | This list is generated from the internal table ApacheTop maintains. Thus, in 101 | this example, the list is being generated from the last 30 seconds of data. You 102 | can see the root page has been requested 103 times in the last 30 seconds, 103 | resulting in about 3.4 hits per second. Additionally, those 103 requests have 104 | resulted in 2983K of traffic, at an average of 99.4K/second. 105 | 106 | You can see the individual number of return codes a given item has generated 107 | by pressing 'n'. This alternates the numbers columns between hits/bytes 108 | and return codes for each item. 109 | 110 | 111 | You may sort this list by any of the first four columns; first press 's' to 112 | enter the 'sort submenu', and then one of the following: 113 | r Sort by REQUESTS 114 | R Sort by REQUESTS/SECOND 115 | b Sort by BYTES 116 | B Sort by BYTES/SECOND 117 | 118 | If you are viewing return code breakdown, then you'll see the following: 119 | 2 2xx 120 | 3 3xx 121 | 4 4xx 122 | 5 5xx 123 | 124 | Thus you can see where all your Page Not Founds are coming from and so on. 125 | Each sort order is individually maintained, so you can sort by 3xx, and 126 | Bytes, for example, then freely switch between number modes (using 'n') 127 | without losing either setting. 128 | 129 | Additionally, you can press d during runtime to switch the list of displayed 130 | items between URLs, IPs, and REFERRERs. URLs is the default, and simply 131 | groups together hits on your site and provides collated stats for each one. 132 | IPs, similarly, groups hits from each IP and shows you stats for it. So you 133 | can see how much bandwidth is being used by any given IP. REFERRERs is handy 134 | if you want to see where your traffic is coming from. The stats here reflect 135 | how many pages/kbytes have been served as a result of a particular referrer. 136 | 137 | To hold the current screen at any time, press p - statistics will still be 138 | generated in the background, but whatever is displayed at the current time 139 | is kept onscreen until you press p again. 140 | 141 | The asterisk beside the URL/IP/Referrer entry in the table can be used to 142 | restrict the display to any entry you're interested in. Use Up/Down arrow 143 | keys to move the asterisk to an entry you're interested in (you can use 'p' 144 | to freeze the display to give you more time to do so) and then press Right 145 | arrow to enter the display specific for that item. 146 | 147 | If the item you expanded is a URL, then IPs and Referrers specific 148 | to that URL will be shown; ie, IPs (or hosts) which are visiting 149 | that URL, and Referrers which are referring people to that URL. 150 | 151 | If the item is an IP/Host, then URLs that IP/Host is visiting will 152 | be displayed, along with the referrers that IP is coming from. 153 | 154 | If the item is a Referrer, then URLs and IPs will be shown which 155 | have that Referrer. 156 | 157 | You may turn off any of these subcolumns; press 't' to enter the toggle 158 | submenu, then: 159 | u Toggles URL subdisplay 160 | r Toggles REFERRER subdisplay 161 | h Toggles HOSTS subdisplay 162 | 163 | Thus you can only display HOSTS that are visiting a given URL, etc. 164 | Use Left arrow to return to the previous display. 165 | 166 | 167 | 168 | 169 | Bug reports and patches are very welcome. Please send any comments on. 170 | (if anyone fancies rewriting this README so its a bit more readable..) 171 | Chris Elsworth 172 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | autoreconf -i -v -f 2 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | # Process this file with autoconf to produce a configure script. 2 | 3 | AC_INIT([apachetop],[0.23.2],[https://github.com/tessus/apachetop/issues]) 4 | 5 | AC_DEFUN([AC_CONFIG_NICE], 6 | [ 7 | test -f $1 && mv $1 $1.old 8 | rm -f $1.old 9 | cat >$1<> $1 20 | fi 21 | done 22 | 23 | for arg in [$]0 "[$]@"; do 24 | echo "'[$]arg' \\" >> $1 25 | done 26 | echo '"[$]@"' >> $1 27 | chmod +x $1 28 | ]) 29 | AC_CONFIG_NICE(config.nice) 30 | 31 | AM_SILENT_RULES([yes]) 32 | AC_CONFIG_SRCDIR(src/apachetop.cc) 33 | 34 | AC_CONFIG_AUX_DIR(config) 35 | 36 | AM_CONFIG_HEADER(config.h) 37 | AM_INIT_AUTOMAKE 38 | 39 | PKG_PROG_PKG_CONFIG 40 | 41 | # Add non-standard directories to the include path 42 | AC_ARG_WITH(libraries, 43 | [ 44 | --with-libraries= additional place to look for libraries], 45 | [LDFLAGS="$LDFLAGS -L $withval"], 46 | , 47 | ) 48 | # Add non-standard includes to the include path 49 | AC_ARG_WITH(includes, 50 | [ 51 | --with-includes= additional place to look for header files], 52 | [CPPFLAGS="$CPPFLAGS -I $withval"], 53 | , 54 | ) 55 | 56 | 57 | # Checks for programs. 58 | AC_PROG_CXX 59 | AC_LANG([C++]) 60 | 61 | # Checks for header files. 62 | AC_HEADER_STDC 63 | AC_CHECK_HEADERS([arpa/inet.h netdb.h netinet/in.h \ 64 | string.h strings.h sys/socket.h time.h sys/time.h \ 65 | limits.h sys/param.h]) 66 | 67 | # Checks for typedefs, structures, and compiler characteristics. 68 | #AC_HEADER_STDBOOL # not in 2.53? 69 | AC_C_CONST 70 | AC_HEADER_TIME 71 | AC_STRUCT_TM 72 | AC_TYPE_OFF_T 73 | AC_TYPE_SIZE_T 74 | AC_SYS_LARGEFILE 75 | 76 | # Checks for library functions. 77 | AC_FUNC_MALLOC 78 | AC_FUNC_VPRINTF 79 | AC_CHECK_FUNCS([inet_aton memset strchr strdup kqueue strerror strstr]) 80 | 81 | # pcre2 {{{ 82 | AC_ARG_WITH(pcre2, 83 | [ --with-pcre2= prefix of pcre2 installation (eg /usr/local)], 84 | [ 85 | CPPFLAGS="$CPPFLAGS -I $withval/include" 86 | LDFLAGS="$LDFLAGS -L $withval/lib" 87 | ] 88 | ) 89 | 90 | AC_CHECK_HEADERS(pcre2.h, 91 | AC_SEARCH_LIBS([pcre2_compile_8], [pcre2-8]) , 92 | AC_MSG_WARN([*** pcre2.h not found -- consider using --with-pcre2]) , 93 | [#define PCRE2_CODE_UNIT_WIDTH 8] 94 | ) 95 | # }}} 96 | 97 | # fam {{{ 98 | AC_ARG_WITH(fam, 99 | [ --with-fam= prefix of fam installation (eg /usr/local)], 100 | [ 101 | CPPFLAGS="$CPPFLAGS -I $withval/include" 102 | LDFLAGS="$LDFLAGS -L $withval/lib" 103 | ] 104 | ) 105 | 106 | AC_CHECK_HEADERS(fam.h, 107 | AC_SEARCH_LIBS([FAMOpen], [fam]) , 108 | AC_MSG_WARN([*** fam.h not found -- consider using --with-fam]) 109 | ) 110 | # }}} 111 | 112 | # adns {{{ 113 | AC_ARG_WITH(adns, 114 | [ --with-adns= prefix of adns installation (eg /usr/local)], 115 | [ 116 | CPPFLAGS="$CPPFLAGS -I $withval/include" 117 | LDFLAGS="$LDFLAGS -L $withval/lib" 118 | ] 119 | ) 120 | 121 | AC_CHECK_HEADERS(adns.h, 122 | AC_SEARCH_LIBS([adns_submit], [adns]) , 123 | AC_MSG_WARN([*** adns.h not found -- consider using --with-adns]) 124 | ) 125 | # }}} 126 | 127 | # --with-logfile {{{ 128 | AC_ARG_WITH(logfile, 129 | [ 130 | --with-logfile= location of default logfile], 131 | [AC_DEFINE_UNQUOTED(DEFAULT_LOGFILE, "$withval", [Optionally override the DEFAULT_LOGFILE in apachetop.h])], 132 | , 133 | ) 134 | # }}} 135 | 136 | AC_SEARCH_LIBS([socket], [socket]) 137 | AC_SEARCH_LIBS([inet_addr], [nsl]) 138 | 139 | NCURSES_FOUND=no 140 | PKG_CHECK_MODULES(NCURSES, ncurses, [ 141 | LIBS="$LIBS $NCURSES_LIBS" 142 | NCURSES_FOUND=yes 143 | ]) 144 | 145 | AS_IF([test "x$NCURSES_FOUND" != "xyes"], [ 146 | AC_SEARCH_LIBS([attron], [ncurses]) 147 | AC_SEARCH_LIBS([tgetstr], [termcap]) 148 | AC_SEARCH_LIBS([mvprintw], [curses ncurses] , 149 | [] , 150 | [ 151 | AC_MSG_ERROR([No useful curses library found!]) 152 | ] 153 | ) 154 | ]) 155 | 156 | AC_SEARCH_LIBS([readline], [readline], 157 | [ 158 | AC_DEFINE(HAVE_READLINE,1,[Define if you have readline library]) 159 | AC_SUBST(HAVE_READLINE) 160 | ] , 161 | [ 162 | AC_MSG_ERROR(readline library not found) 163 | ] 164 | ) 165 | 166 | 167 | # everything is in CPPFLAGS up to this point, now we move to CXXFLAGS 168 | # this is mostly done for pcre2 test, AC_CHECK_HEADERS wants to use CPP 169 | #CXXFLAGS="$CXXFLAGS $CPPFLAGS" 170 | AC_SUBST(CXXFLAGS) 171 | 172 | AC_CONFIG_FILES([Makefile man/Makefile src/Makefile]) 173 | AC_OUTPUT 174 | -------------------------------------------------------------------------------- /man/Makefile.am: -------------------------------------------------------------------------------- 1 | ## Process this file with automake to produce Makefile.in. 2 | 3 | man_MANS = apachetop.1 4 | 5 | EXTRA_DIST = $(man_MANS) 6 | 7 | MAINTAINERCLEANFILES = Makefile.in 8 | -------------------------------------------------------------------------------- /man/apachetop.1: -------------------------------------------------------------------------------- 1 | .TH apachetop 1 "June, 2015" "version 0.15.6" "USER COMMANDS" 2 | .SH NAME 3 | apachetop \- display real-time web server statistics 4 | .SH SYNOPSIS 5 | .B apachetop [-f filename] [-H hits | -T time] [-q] [-l] [-s segments] [-p] [-r secs] 6 | .SH DESCRIPTION 7 | ApacheTop watches a logfile generated by Apache (in standard common or 8 | combined logformat, and generates human-parsable output in realtime. 9 | .OPTIONS 10 | .TP 11 | -f logfile 12 | Select which file to watch. 13 | Specify this option multiple times to watch multiple files. 14 | .TP 15 | -H hits | -T time 16 | These options are mutually exclusive. Specify only one, if any at 17 | all. They work as follows. ApacheTop maintains a table of 18 | information internally containing all the relevant information about 19 | the hits it's seen. This table can only be a finite size, so you 20 | need to decide how big it's going to be. You have two options. 21 | You can either: 22 | Use -H to say "remember hits" 23 | or Use -T to say "remember all hits in seconds" 24 | The default (at the moment) is to remember hits for 30 seconds. 25 | Setting this too large (whichever option you choose) will cause 26 | ApacheTop to use more memory and more CPU time. My experimentation 27 | finds that remembering no more than around 5000 requests works well. 28 | .TP 29 | -q 30 | Instructs ApacheTop to keep the querystrings, not remove them. 31 | .TP 32 | -l 33 | Instructs ApacheTop to lowercase all URLs, thus /FOO and /foo are 34 | treated as the same and accumulate the same statistics. 35 | .TP 36 | -s segments 37 | Instructs ApacheTop to only keep the first parts of the 38 | path. Trailing slashes are kept if present. Statistics are then 39 | merged for each truncated url. 40 | .TP 41 | -p 42 | Instructs ApacheTop to keep the protocol (http:// usually) at the 43 | front of its' referrer strings. Normal behaviour is to remove them 44 | to give more room to more useful information. 45 | .TP 46 | -r secs 47 | Set default refresh delay, in seconds. 48 | .SH EXAMPLES 49 | .TP 50 | apachetop -f /var/logs/httpd/access.log 51 | .SH AUTHOR 52 | Chris Elsworth 53 | .SH REPORTING BUGS 54 | Report bugs at: https://github.com/tessus/apachetop/issues 55 | .SH SEE ALSO 56 | https://www.cae.me.uk/projects/apachetop 57 | -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | ## Process this file with automake to produce Makefile.in. 2 | 3 | bin_PROGRAMS = apachetop 4 | 5 | apachetop_SOURCES = apachetop.cc log.cc inlines.cc ohtbl.cc map.cc queue.cc \ 6 | display.cc hits_circle.cc timed_circle.cc filters.cc \ 7 | resolver.cc 8 | 9 | 10 | noinst_HEADERS = apachetop.h log.h ohtbl.h map.h queue.h \ 11 | display.h hits_circle.h circle.h timed_circle.h filters.h \ 12 | resolver.h pcre2_cpp_wrapper.h 13 | 14 | MAINTAINERCLEANFILES = Makefile.in 15 | -------------------------------------------------------------------------------- /src/apachetop.cc: -------------------------------------------------------------------------------- 1 | /* APACHETOP 2 | ** 3 | ** 4 | */ 5 | #include "apachetop.h" 6 | 7 | /* die and report why */ 8 | #define DIE(msg) fprintf(stderr, "%s: %s\n", msg, strerror(errno)); catchsig(1); 9 | /* die with no strerror */ 10 | #define DIE_N(msg) fprintf(stderr, "%s\n", msg); catchsig(1); 11 | 12 | #if HAVE_ADNS_H 13 | /* resolver master state */ 14 | adns_state adns; 15 | #endif 16 | 17 | /* global stats */ 18 | struct gstat gstats; 19 | 20 | time_t now; 21 | 22 | struct itemlist *items = NULL; 23 | map *last_display_map; 24 | 25 | Circle *c; 26 | Timed_Circle *tc; 27 | Hits_Circle *hc; 28 | 29 | map *um, /* urlmap */ 30 | *im, /* ipmap */ 31 | *hm, /* hostmap */ 32 | *rm, /* referrermap */ 33 | *fm; /* filemap */ 34 | 35 | struct config cf; 36 | 37 | /* inputs */ 38 | struct input *in; 39 | 40 | WINDOW *win; 41 | 42 | #if (POLLING_METHOD == USING_KQUEUE) 43 | int kq; 44 | #elif (POLLING_METHOD == USING_FAM) 45 | /* master fd for talking to FAM */ 46 | FAMConnection fc; 47 | #endif 48 | 49 | int main(int argc, char *argv[]) 50 | { 51 | int fd, buflen, ch, x; 52 | char buf[32768], *bufcp, *nextline; 53 | time_t last_display = 0; 54 | struct logbits lb; 55 | struct input *curfile; 56 | bool seen_h = false, seen_t = false; 57 | extern char *optarg; 58 | #if (POLLING_METHOD == USING_KQUEUE) 59 | /* we have and are using kqueue */ 60 | struct timespec stv; 61 | struct kevent *ev, *evptr; 62 | #elif (POLLING_METHOD == USING_FAM) 63 | /* we have and are using FAM */ 64 | int fam_fd; 65 | FAMEvent fe; 66 | struct timeval tv; 67 | tv.tv_sec = 1; tv.tv_usec = 0; 68 | fd_set readfds; 69 | 70 | #else /* stat() then */ 71 | struct stat sb; 72 | struct timeval tv; 73 | tv.tv_sec = 1; tv.tv_usec = 0; 74 | #endif 75 | 76 | LogParser *p; 77 | CommonLogParser *cmmp; //AtopLogParser *atpp; 78 | 79 | 80 | /* some init stuff will need to know the time */ 81 | now = time(NULL); 82 | 83 | /* set up initial configuration {{{ */ 84 | memset(&cf, 0, sizeof(cf)); 85 | cf.debug = true; 86 | cf.current_display_size = 0; 87 | cf.input_count = 0; 88 | cf.circle_size = DEFAULT_CIRCLE_SIZE; 89 | cf.circle_mode = DEFAULT_CIRCLE_MODE; 90 | cf.sort = DEFAULT_SORT; 91 | cf.retcodes_sort = DEFAULT_RETCODES_SORT; 92 | cf.refresh_delay = DEFAULT_REFRESH_DELAY; 93 | cf.display_mode = DEFAULT_DISPLAY_MODE; 94 | cf.numbers_mode = DEFAULT_NUMBERS_MODE; 95 | cf.detail_display_urls = true; 96 | cf.detail_display_hosts = true; 97 | cf.detail_display_refs = true; 98 | cf.display_paused = false; 99 | cf.keep_querystring = false; 100 | cf.lowercase_urls = false; 101 | cf.keep_segments = 0; 102 | cf.preserve_ref_protocol = 0; 103 | cf.do_immed_display = false; 104 | cf.do_resolving = false; 105 | cf.urlfilter = new Filter(); 106 | cf.reffilter = new Filter(); 107 | cf.hostfilter = new Filter(); 108 | /* }}} */ 109 | 110 | /* support MAX_INPUT_FILES input files */ 111 | in = (struct input *)calloc(MAX_INPUT_FILES, sizeof(struct input)); 112 | 113 | #if (POLLING_METHOD == USING_KQUEUE) /* then create kqueue {{{ */ 114 | if ((kq = kqueue()) < 0) 115 | { 116 | DIE("cannot create kqueue"); 117 | exit(1); 118 | } 119 | 120 | /* space in ev for all inputs */ 121 | ev = (struct kevent *)calloc(MAX_INPUT_FILES, sizeof(struct kevent)); 122 | /* }}} */ 123 | #elif (POLLING_METHOD == USING_FAM) /* open FAM connection */ 124 | if (FAMOpen(&fc)) 125 | { 126 | DIE("cannot connect to fam"); 127 | exit(1); 128 | } 129 | 130 | fam_fd = FAMCONNECTION_GETFD(&fc); 131 | #endif 132 | 133 | #if HAVE_ADNS_H 134 | /* open adns connection */ 135 | adns_init(&adns, adns_if_noerrprint, 0); 136 | #endif 137 | 138 | /* process commandline {{{ */ 139 | while ((ch = getopt(argc, argv, "f:H:T:hvqlrs:pd:")) != -1) 140 | { 141 | switch(ch) 142 | { 143 | case 'f': 144 | if (new_file(optarg, SEEK_TO_END) == -1) 145 | { 146 | fprintf(stderr, "opening %s: %s\n", 147 | optarg, strerror(errno)); 148 | sleep(2); 149 | } 150 | else 151 | cf.input_count++; 152 | break; 153 | case 'T': 154 | x = atoi(optarg); 155 | seen_t = true; 156 | if (x > 0) 157 | { 158 | cf.circle_mode = TIMED_CIRCLE; 159 | cf.circle_size = x; 160 | } 161 | break; 162 | case 'H': 163 | x = atoi(optarg); 164 | seen_h = true; 165 | if (x > 0) 166 | { 167 | cf.circle_mode = HITS_CIRCLE; 168 | cf.circle_size = x; 169 | } 170 | break; 171 | 172 | case 'q': 173 | cf.keep_querystring = true; 174 | break; 175 | 176 | case 'l': 177 | cf.lowercase_urls = true; 178 | break; 179 | 180 | case 'r': 181 | cf.do_resolving = true; 182 | break; 183 | 184 | case 's': 185 | x = atoi(optarg); 186 | if (x > 0) 187 | cf.keep_segments = x; 188 | break; 189 | 190 | case 'p': 191 | cf.preserve_ref_protocol = 1; 192 | break; 193 | 194 | case 'd': 195 | x = atoi(optarg); 196 | if (x > 0) 197 | cf.refresh_delay = x; 198 | break; 199 | 200 | case 'v': 201 | version(); 202 | exit(0); 203 | break; 204 | 205 | case 'h': 206 | case '?': 207 | usage(); 208 | exit(1); 209 | break; 210 | } 211 | } /* }}} */ 212 | 213 | /* if no files have been specified, we'll use DEFAULT_LOGFILE */ 214 | if (cf.input_count == 0) 215 | { 216 | if (new_file(DEFAULT_LOGFILE, SEEK_TO_END) != -1) 217 | cf.input_count++; 218 | 219 | /* if it's still zero, fail */ 220 | if (cf.input_count == 0) 221 | { 222 | fprintf(stderr, "opening %s: %s\n", 223 | DEFAULT_LOGFILE, strerror(errno)); 224 | DIE_N("No input files could be opened"); 225 | exit(1); 226 | } 227 | } 228 | 229 | if (seen_t && seen_h) 230 | { 231 | DIE_N("-T and -H are mutually exclusive. Specify only one."); 232 | exit(1); 233 | } 234 | 235 | 236 | /* set up circle */ 237 | switch(cf.circle_mode) 238 | { 239 | default: 240 | case HITS_CIRCLE: 241 | hc = new Hits_Circle; 242 | hc->create(cf.circle_size); 243 | c = hc; 244 | break; 245 | 246 | case TIMED_CIRCLE: 247 | tc = new Timed_Circle; 248 | tc->create(cf.circle_size); 249 | c = tc; 250 | break; 251 | } 252 | 253 | /* set up one of each parser */ 254 | /* CommonLogParser handles combined too */ 255 | cmmp = new CommonLogParser; 256 | #if 0 257 | /* this isn't written yet */ 258 | atpp = new AtopLogParser; 259 | #endif 260 | 261 | /* maps auto-resize, just use a sane starting point {{{ */ 262 | /* url string -> url hash map */ 263 | um = new map; /* hashing happens internally */ 264 | um->create(cf.circle_size); 265 | 266 | /* referrer string -> hash map */ 267 | rm = new map; 268 | rm->create(cf.circle_size); 269 | 270 | /* ip string -> ip hash map */ 271 | im = new map; 272 | im->create(cf.circle_size); 273 | 274 | /* host string -> host hash map */ 275 | hm = new map; 276 | hm->create(cf.circle_size); 277 | /* }}} */ 278 | 279 | memset(&gstats, 0, sizeof(gstats)); 280 | gstats.start = time(NULL); 281 | 282 | signal(SIGINT, &catchsig); 283 | signal(SIGKILL, &catchsig); 284 | signal(SIGWINCH, &catchwinch); 285 | 286 | win = initscr(); 287 | nonl(); /* no NL->CR/NL on output */ 288 | cbreak(); /* enable reading chars one at a time */ 289 | noecho(); 290 | keypad(win, true); 291 | 292 | nodelay(win, true); 293 | 294 | for( ;; ) 295 | { 296 | if (cf.exit) break; 297 | 298 | now = time(NULL); 299 | 300 | /* periodically check that we're not in a submenu, and we 301 | * haven't been for more than 5 seconds without any more 302 | * keypresses; but if cf.in_submenu_stay is true, we stay in 303 | * the submenu until we receive a keypress (in read_key) */ 304 | if (cf.in_submenu && 305 | !cf.in_submenu_stay && 306 | now - cf.in_submenu_time > 5) 307 | { 308 | cf.in_submenu = SUBMENU_NONE; 309 | clear_submenu_banner(); 310 | } 311 | 312 | /* if we have any input files waiting to be opened, try to */ 313 | for (x = 0 ; x < MAX_INPUT_FILES ; ++x) 314 | { 315 | /* for each entry in input .. */ 316 | curfile = &in[x]; 317 | if (curfile->inode && curfile->open == false) 318 | { 319 | dprintf("%s needs reopening\n", 320 | curfile->filename); 321 | 322 | new_file(curfile->filename, NO_SEEK_TO_END); 323 | 324 | /* what happens if a new fd is used? the old 325 | * struct will still have inode > 0 and 326 | * open == false so we'll keep coming back 327 | * here */ 328 | } 329 | } 330 | 331 | 332 | #if HAVE_ADNS_H 333 | /* see if adns has got anything for us */ 334 | collect_dns_responses(); 335 | #endif 336 | 337 | /* see if it's time to update.. */ 338 | if (display(last_display)) 339 | { 340 | /* returns true when we updated, so.. */ 341 | last_display = now; // remember our last update 342 | } 343 | 344 | /* start of for() loop within these folds.. */ 345 | #if (POLLING_METHOD == USING_KQUEUE) /* {{{ */ 346 | /* every 1/10th of a second */ 347 | stv.tv_sec = 0; stv.tv_nsec = 10000000; 348 | x = kevent(kq, NULL, 0, ev, MAX_INPUT_FILES, &stv); 349 | // if (x == 0) continue; /* timeout, nothing happened */ 350 | if (x < 0) break; /* error */ 351 | 352 | for(evptr = ev ; evptr->filter ; evptr++) 353 | { 354 | /* udata contains a pointer to the struct input 355 | ** array element for this fd 356 | */ 357 | curfile = (struct input *)evptr->udata; 358 | fd = curfile->fd; /* or evptr->ident will work */ 359 | 360 | /* see if we can deduce what happened */ 361 | if (((evptr->fflags & NOTE_DELETE) == NOTE_DELETE) || 362 | ((evptr->fflags & NOTE_RENAME) == NOTE_RENAME)) 363 | { 364 | /* file deleted or renamed */ 365 | close(fd); 366 | curfile->open = false; 367 | 368 | /* skip rest of loop, we'll try 369 | * to reopen at top of for(;;) */ 370 | continue; 371 | } 372 | else if ((evptr->fflags & NOTE_WRITE) == NOTE_WRITE) 373 | { 374 | //read_amt = ev.data; /* we don't use this */ 375 | } 376 | /* }}} */ 377 | #elif (POLLING_METHOD == USING_FAM) /* {{{ */ 378 | FD_ZERO(&readfds); 379 | FD_SET(fam_fd, &readfds); 380 | 381 | /* 1/10th of a second */ 382 | tv.tv_sec = 0; 383 | tv.tv_usec = 100000; 384 | 385 | /* perform a select() on the fam filedescriptor */ 386 | select(fam_fd + 1, &readfds, NULL, NULL, &tv); 387 | 388 | /* if FAM has nothing to report, loop around */ 389 | if (!(FD_ISSET(fam_fd, &readfds))) 390 | continue; 391 | 392 | while (FAMPending(&fc) == 1 && FAMNextEvent(&fc, &fe)) 393 | { 394 | /* fe.userdata is struct input * element for this 395 | ** file; see new_file() for how it's set 396 | */ 397 | curfile = (struct input *)fe.userdata; 398 | fd = curfile->fd; 399 | 400 | /* }}} */ 401 | #else /* fallback to stat() {{{ */ 402 | 403 | /* 1/10th of a second */ 404 | tv.tv_sec = 0; tv.tv_usec = 100000; 405 | select(0, NULL, NULL, NULL, &tv); 406 | 407 | /* check every file */ 408 | for (x = 0 ; x < MAX_INPUT_FILES ; ++x) 409 | { 410 | curfile = &in[x]; 411 | fd = curfile->fd; 412 | 413 | if (curfile->open == false || fd == 0) 414 | continue; 415 | 416 | if (stat(curfile->filename, &sb) == -1) 417 | { 418 | /* file removed */ 419 | close(fd); 420 | curfile->fd = 0; 421 | curfile->open = false; 422 | 423 | /* skip rest of loop, we'll try 424 | * to reopen at top of for(;;) */ 425 | continue; 426 | } 427 | else 428 | { 429 | /* file still there, but lets check the 430 | * inode hasn't changed (ie, it's been 431 | * recreated under our feet */ 432 | if (sb.st_ino != curfile->inode) 433 | { 434 | close(fd); 435 | curfile->open = false; 436 | 437 | /* skip rest of loop, we'll try 438 | * to reopen at top of for(;;) */ 439 | continue; 440 | } 441 | } 442 | #endif /* }}} */ 443 | 444 | /* read the data */ 445 | buflen = read(fd, buf, sizeof(buf)); 446 | 447 | if (buflen == 0) /* no data */ 448 | { 449 | /* this should only happen if we've had to 450 | * fall back to stat() */ 451 | continue; 452 | } 453 | 454 | if (buflen < 0) /* error */ 455 | { 456 | DIE("read"); 457 | } 458 | 459 | curfile->lastreq = now; 460 | 461 | /* pass each line to logsplit; FIXME tidy this up */ 462 | nextline = bufcp = buf; 463 | while ((nextline - buf) < buflen && nextline) 464 | { 465 | bufcp = nextline; 466 | /* find the end of this line */ 467 | if (!(nextline = strchr(bufcp, '\n'))) 468 | { 469 | /* we can't, ignore it */ 470 | continue; 471 | } 472 | 473 | *nextline = '\0'; 474 | ++nextline; 475 | 476 | /* which parser? */ 477 | #if CHRIS_HAS_WRITTEN_MORE_PARSERS 478 | switch(in->type) 479 | { 480 | /* only one atm */ 481 | default: 482 | p = cmmp; 483 | break; 484 | } 485 | #else 486 | p = cmmp; 487 | #endif 488 | 489 | if (p->parse(bufcp, &lb) == 0) 490 | { 491 | /* record which file the log is from */ 492 | lb.fileid = fd; 493 | 494 | /* insert into circle */ 495 | c->insert(lb); 496 | 497 | /* record stats */ 498 | recordstats(lb); 499 | } 500 | // next line.. 501 | 502 | } /* while ((nl - buf) < buflen && nl) */ 503 | } /* for(evptr = ev ; evptr->filter ; evptr++) */ 504 | /* while (FAMPending(&fc) == 1 && FAMNextEvent(&fc, &fe)) */ 505 | /* for (i = 0 ; i < MAX_INPUT_FILES ; ++i) */ 506 | 507 | /* check for keypresses */ 508 | if ((ch = wgetch(win)) != ERR) 509 | read_key(ch); 510 | 511 | } /* for( ;; ) */ 512 | 513 | delete cmmp; 514 | endwin(); 515 | 516 | return 0; 517 | } 518 | 519 | int recordstats(struct logbits l) /* {{{ */ 520 | { 521 | int t; 522 | 523 | /* global stats */ 524 | if (gstats.alltime.first == 0) gstats.alltime.first = now; 525 | gstats.alltime.last = now; 526 | gstats.alltime.reqcount++; 527 | gstats.alltime.bytecount += l.bytes; 528 | 529 | /* for this return code, increment */ 530 | t = (int)(l.retcode/100); 531 | gstats.r_codes[t].reqcount++; 532 | gstats.r_codes[t].bytecount += l.bytes; 533 | 534 | return 0; 535 | } /* }}} */ 536 | 537 | int read_key(int ch) /* {{{ */ 538 | { 539 | #define SUBMENU_SORT_HB_TITLE "sort by.." 540 | #define SUBMENU_SORT_HB_BANNER "r) REQUESTS R) REQS/SEC b) BYTES B) BYTES/SEC" 541 | #define SUBMENU_SORT_RC_TITLE "sort by.." 542 | #define SUBMENU_SORT_RC_BANNER "2) 2xx 3) 3xx 4) 4xx 5) 5xx" 543 | 544 | #define SUBMENU_DISP_TITLE "toggle subdisplay.." 545 | #define SUBMENU_DISP_BANNER "u) URLS r) REFERRERS h) HOSTS" 546 | 547 | #define SUBMENU_FILT_TITLE "filters.." 548 | #define SUBMENU_FILT_BANNER "a) add/edit menu c) clear all s) show active" 549 | 550 | #define SUBMENU_FILT_ADD_TITLE "filters: add.." 551 | #define SUBMENU_FILT_ADD_BANNER "u) to URLS r) to REFERRERS h) to HOSTS" 552 | 553 | /* got a keypress, best process it */ 554 | 555 | /* check submenu state; in no submenu we do the master list.. */ 556 | if (cf.in_submenu == SUBMENU_NONE) /* {{{ */ 557 | switch(ch) 558 | { 559 | case 's': /* sort .. submenus */ 560 | cf.in_submenu_time = now; 561 | cf.in_submenu_stay = false; 562 | 563 | switch(cf.numbers_mode) 564 | { 565 | default: 566 | case NUMBERS_HITS_BYTES: 567 | cf.in_submenu = SUBMENU_SORT_HB; 568 | /* display banner to aid further 569 | * presses */ 570 | display_submenu_banner( 571 | SUBMENU_SORT_HB_TITLE, 572 | sizeof(SUBMENU_SORT_HB_TITLE), 573 | SUBMENU_SORT_HB_BANNER); 574 | break; 575 | 576 | case NUMBERS_RETCODES: 577 | cf.in_submenu = SUBMENU_SORT_RC; 578 | /* display banner to aid further 579 | * presses */ 580 | display_submenu_banner( 581 | SUBMENU_SORT_RC_TITLE, 582 | sizeof(SUBMENU_SORT_RC_TITLE), 583 | SUBMENU_SORT_RC_BANNER); 584 | break; 585 | } 586 | 587 | break; 588 | 589 | case 't': /* toggle displays */ 590 | cf.in_submenu = SUBMENU_DISP; 591 | cf.in_submenu_time = now; 592 | cf.in_submenu_stay = false; 593 | 594 | /* display banner to aid further presses */ 595 | display_submenu_banner(SUBMENU_DISP_TITLE, 596 | sizeof(SUBMENU_DISP_TITLE), SUBMENU_DISP_BANNER); 597 | break; 598 | 599 | case 'f': /* filter submenu */ 600 | cf.in_submenu = SUBMENU_FILT; 601 | cf.in_submenu_time = now; 602 | cf.in_submenu_stay = false; 603 | 604 | /* display banner to aid further presses */ 605 | display_submenu_banner(SUBMENU_FILT_TITLE, 606 | sizeof(SUBMENU_FILT_TITLE), SUBMENU_FILT_BANNER); 607 | 608 | break; 609 | 610 | case 'h': /* enter help; this is a special submenu */ 611 | case '?': 612 | cf.in_submenu = SUBMENU_HELP; 613 | cf.in_submenu_time = now; 614 | 615 | /* stay in here till next keypress */ 616 | cf.in_submenu_stay = true; 617 | 618 | /* don't blat it */ 619 | cf.display_paused = true; 620 | 621 | display_help(); 622 | 623 | break; 624 | 625 | case 'p': /* (un)pause display */ 626 | cf.display_paused = !cf.display_paused; 627 | if (cf.display_paused) 628 | { 629 | /* tell the user we're paused */ 630 | DRAW_PAUSED(0,60); /* macro in display.h */ 631 | refresh(); 632 | } 633 | else 634 | { 635 | /* turning off pause forces an update */ 636 | cf.do_immed_display = true; 637 | } 638 | break; 639 | 640 | case ' ': 641 | cf.do_immed_display = true; 642 | break; 643 | 644 | #if 0 645 | case 's': /* new refresh delay */ 646 | #ifndef SOLARIS /* linking against readline is failing on Solaris atm */ 647 | endwin(); 648 | char *t; 649 | int nd; 650 | t = readline("Seconds to Delay: "); 651 | if ((nd = atoi(t)) != 0) 652 | cf.refresh_delay = nd; 653 | 654 | refresh(); 655 | #endif 656 | break; 657 | #endif 658 | case 'd': /* display mode; urls or hosts */ 659 | switch(cf.display_mode) 660 | { 661 | case DISPLAY_URLS: 662 | cf.display_mode = DISPLAY_HOSTS; 663 | break; 664 | case DISPLAY_HOSTS: 665 | cf.display_mode = DISPLAY_REFS; 666 | break; 667 | case DISPLAY_REFS: 668 | cf.display_mode = DISPLAY_URLS; 669 | break; 670 | } 671 | cf.do_immed_display = true; 672 | break; 673 | 674 | case 'n': /* numbers mode; hits/bytes or returncodes */ 675 | switch(cf.numbers_mode) 676 | { 677 | case NUMBERS_HITS_BYTES: 678 | cf.numbers_mode = NUMBERS_RETCODES; 679 | break; 680 | case NUMBERS_RETCODES: 681 | cf.numbers_mode = NUMBERS_HITS_BYTES; 682 | break; 683 | } 684 | cf.do_immed_display = true; 685 | break; 686 | 687 | case KEY_UP: 688 | if (cf.display_mode != DISPLAY_DETAIL) 689 | { 690 | /* sanity checking for this is done 691 | * in drawMarker() */ 692 | cf.selected_item_screen--; 693 | drawMarker(); 694 | translate_screen_to_pos(); 695 | } 696 | break; 697 | #if 0 698 | case KEY_END: 699 | case KEY_NPAGE: 700 | if (cf.display_mode != DISPLAY_DETAIL) 701 | { 702 | cf.selected_item_screen = 9999; 703 | drawMarker(); 704 | translate_screen_to_pos(); 705 | } 706 | break; 707 | #endif 708 | case KEY_DOWN: 709 | if (cf.display_mode != DISPLAY_DETAIL) 710 | { 711 | /* sanity checking for this is done 712 | * in drawMarker() */ 713 | cf.selected_item_screen++; 714 | drawMarker(); 715 | translate_screen_to_pos(); 716 | } 717 | break; 718 | 719 | case KEY_RIGHT: 720 | if (cf.display_mode != DISPLAY_DETAIL) 721 | { 722 | /* enter detailed display mode */ 723 | cf.display_mode_detail = cf.display_mode; 724 | cf.display_mode = DISPLAY_DETAIL; 725 | cf.do_immed_display = true; 726 | } 727 | break; 728 | 729 | case KEY_LEFT: 730 | if (cf.display_mode == DISPLAY_DETAIL) 731 | { 732 | /* leave detailed display mode */ 733 | cf.display_mode = cf.display_mode_detail; 734 | cf.do_immed_display = true; 735 | } 736 | break; 737 | 738 | case KEY_REFRESH: 739 | refresh(); 740 | break; 741 | 742 | case KEY_BREAK: 743 | case 'q': 744 | cf.exit = true; 745 | break; 746 | } /* }}} */ 747 | 748 | else if (cf.in_submenu == SUBMENU_SORT_HB) /* {{{ */ 749 | { 750 | switch(ch) 751 | { 752 | case 'r': 753 | cf.sort = SORT_REQCOUNT; 754 | break; 755 | 756 | case 'R': 757 | cf.sort = SORT_REQPERSEC; 758 | break; 759 | 760 | case 'b': 761 | cf.sort = SORT_BYTECOUNT; 762 | break; 763 | 764 | case 'B': 765 | cf.sort = SORT_BYTESPERSEC; 766 | break; 767 | 768 | } 769 | cf.in_submenu = SUBMENU_NONE; 770 | clear_submenu_banner(); 771 | cf.do_immed_display = true; 772 | } /* }}} */ 773 | else if (cf.in_submenu == SUBMENU_SORT_RC) /* {{{ */ 774 | { 775 | switch(ch) 776 | { 777 | case '2': 778 | cf.retcodes_sort = SORT_2XX; 779 | break; 780 | case '3': 781 | cf.retcodes_sort = SORT_3XX; 782 | break; 783 | case '4': 784 | cf.retcodes_sort = SORT_4XX; 785 | break; 786 | case '5': 787 | cf.retcodes_sort = SORT_5XX; 788 | break; 789 | } 790 | cf.in_submenu = SUBMENU_NONE; 791 | clear_submenu_banner(); 792 | cf.do_immed_display = true; 793 | } /* }}} */ 794 | 795 | else if (cf.in_submenu == SUBMENU_DISP) /* {{{ */ 796 | { 797 | switch(ch) 798 | { 799 | case 'u': 800 | cf.detail_display_urls = !cf.detail_display_urls; 801 | break; 802 | 803 | case 'r': 804 | cf.detail_display_refs = !cf.detail_display_refs; 805 | break; 806 | 807 | case 'h': 808 | case 'i': 809 | cf.detail_display_hosts = !cf.detail_display_hosts; 810 | break; 811 | 812 | } 813 | cf.in_submenu = SUBMENU_NONE; 814 | clear_submenu_banner(); 815 | cf.do_immed_display = true; 816 | } /* }}} */ 817 | 818 | else if (cf.in_submenu == SUBMENU_FILT) /* {{{ */ 819 | switch(ch) 820 | { 821 | default: /* default action is to back out */ 822 | cf.in_submenu = SUBMENU_NONE; 823 | clear_submenu_banner(); 824 | cf.do_immed_display = true; 825 | break; 826 | 827 | case 'a': /* add filter.. */ 828 | /* clean away existing menu */ 829 | clear_submenu_banner(); 830 | 831 | cf.in_submenu = SUBMENU_FILT_ADD; 832 | cf.in_submenu_time = now; 833 | cf.in_submenu_stay = false; 834 | 835 | /* display banner to aid further presses */ 836 | display_submenu_banner(SUBMENU_FILT_ADD_TITLE, 837 | sizeof(SUBMENU_FILT_ADD_TITLE), 838 | SUBMENU_FILT_ADD_BANNER); 839 | 840 | break; 841 | 842 | case 'c': /* clear all filters */ 843 | cf.urlfilter->empty(); 844 | cf.hostfilter->empty(); 845 | cf.reffilter->empty(); 846 | 847 | cf.in_submenu = SUBMENU_NONE; 848 | clear_submenu_banner(); 849 | cf.do_immed_display = true; 850 | break; 851 | 852 | case 's': /* show filter page */ 853 | 854 | cf.in_submenu = SUBMENU_FILT_SHOW; 855 | cf.in_submenu_time = now; 856 | 857 | /* stay in here till next keypress */ 858 | cf.in_submenu_stay = true; 859 | 860 | /* don't blat it */ 861 | cf.display_paused = true; 862 | 863 | /* and render the page; similar to display_help(); 864 | * this calls functions in the Filter class to do 865 | * its work */ 866 | display_active_filters(); 867 | 868 | 869 | break; 870 | 871 | } /* }}} */ 872 | else if (cf.in_submenu == SUBMENU_FILT_ADD) /* {{{ */ 873 | { 874 | char *input = NULL; 875 | 876 | /* check the keypress was valid .. */ 877 | switch(ch) 878 | { 879 | case 'u': case 'r': case 'h': 880 | /* it was */ 881 | break; 882 | 883 | default: 884 | cf.in_submenu = SUBMENU_NONE; 885 | cf.in_submenu_stay = false; 886 | clear_submenu_banner(); 887 | cf.do_immed_display = true; 888 | return 0; 889 | break; 890 | } 891 | 892 | /* get an expression */ 893 | 894 | /* do not leave until readline is done */ 895 | cf.in_submenu_stay = true; 896 | /* do not update the display, because we're endwin()'ed */ 897 | cf.display_paused = true; 898 | 899 | endwin(); 900 | 901 | input = readline("Filter: "); 902 | 903 | /* back into curses mode */ 904 | refresh(); 905 | nonl(); /* no NL->CR/NL on output */ 906 | cbreak(); /* enable reading chars one at a time */ 907 | noecho(); 908 | 909 | /* update again */ 910 | cf.in_submenu = SUBMENU_NONE; 911 | cf.in_submenu_stay = false; 912 | cf.display_paused = false; 913 | cf.do_immed_display = true; 914 | 915 | if (!(input && *input)) 916 | { 917 | return 0; 918 | } 919 | 920 | add_history(input); 921 | 922 | /* apply to the appropriate filter */ 923 | switch(ch) 924 | { 925 | case 'u': 926 | cf.urlfilter->store(input); 927 | break; 928 | 929 | case 'h': 930 | cf.hostfilter->store(input); 931 | break; 932 | 933 | case 'r': 934 | cf.reffilter->store(input); 935 | break; 936 | } 937 | free(input); 938 | 939 | } /* }}} */ 940 | 941 | /* special submenus which take over the entire screen */ 942 | else if (cf.in_submenu == SUBMENU_HELP || 943 | cf.in_submenu == SUBMENU_FILT_SHOW) 944 | /* {{{ */ 945 | { 946 | /* any key in these special "submenus" exits them */ 947 | cf.in_submenu = SUBMENU_NONE; 948 | cf.in_submenu_stay = false; 949 | 950 | cf.display_paused = false; 951 | cf.do_immed_display = true; 952 | } 953 | /* }}} */ 954 | 955 | return 0; 956 | } /* }}} */ 957 | 958 | 959 | int new_file(const char *filename, bool do_seek_to_end) /* {{{ */ 960 | /* opens the filename supplied, 961 | * and fills in the struct input passed by reference 962 | */ 963 | { 964 | int i, fd, input_element = -1; 965 | struct stat sb; 966 | struct input *this_file; 967 | char realfile[MAXPATHLEN]; 968 | 969 | /* if realpath cannot resolve the file, give up */ 970 | if ((realpath(filename, realfile) == NULL)) 971 | return -1; 972 | 973 | #if (POLLING_METHOD == USING_KQUEUE) 974 | struct kevent kev; 975 | #endif 976 | 977 | /* stat the item; ensure it's a file and get an inode */ 978 | if (stat(realfile, &sb) == -1) 979 | return -1; 980 | 981 | /* we can't tail anything except a file */ 982 | // if (sb.st_mode != S_IFREG) 983 | // return -1; 984 | 985 | /* choose where to put this input in the struct */ 986 | /* if it has an existing slot, re-use */ 987 | for(i = 0 ; i < cf.input_count ; ++i) 988 | { 989 | if ((strcmp(in[i].filename, filename)) == 0) 990 | { 991 | input_element = i; 992 | break; 993 | } 994 | } 995 | 996 | if (input_element == -1) 997 | { 998 | /* new open, make sure we have room in our arrays */ 999 | if (cf.input_count == MAX_INPUT_FILES) 1000 | { 1001 | DIE_N("Only 50 files are supported at the moment"); 1002 | } 1003 | 1004 | /* add it on at the end */ 1005 | input_element = cf.input_count; 1006 | } 1007 | 1008 | if ((fd = open(realfile, O_RDONLY)) == -1) 1009 | return -1; 1010 | 1011 | this_file = &in[input_element]; 1012 | this_file->fd = fd; 1013 | 1014 | if (do_seek_to_end) lseek(fd, 0, SEEK_END); 1015 | 1016 | if (this_file->filename) free(this_file->filename); 1017 | 1018 | this_file->inode = sb.st_ino; 1019 | this_file->filename = strdup(realfile); 1020 | this_file->lastreq = now; 1021 | this_file->type = LOG_COMMON; /* assumption */ 1022 | this_file->open = true; 1023 | 1024 | #if (POLLING_METHOD == USING_KQUEUE) 1025 | /* add into kqueue */ 1026 | #ifdef __NetBSD__ 1027 | EV_SET(&kev, fd, EVFILT_VNODE, 1028 | EV_ADD | EV_ENABLE | EV_CLEAR, 1029 | NOTE_WRITE | NOTE_DELETE | NOTE_RENAME, 0, (intptr_t)this_file); 1030 | #else 1031 | EV_SET(&kev, fd, EVFILT_VNODE, 1032 | EV_ADD | EV_ENABLE | EV_CLEAR, 1033 | NOTE_WRITE | NOTE_DELETE | NOTE_RENAME, 0, this_file); 1034 | #endif 1035 | if (kevent(kq, &kev, 1, NULL, 0, NULL) < 0) 1036 | { 1037 | DIE("cannot create kevent"); 1038 | } 1039 | #elif (POLLING_METHOD == USING_FAM) 1040 | /* add into FAM */ 1041 | FAMMonitorFile(&fc, realfile, &this_file->famreq, this_file); 1042 | #endif 1043 | 1044 | return 0; 1045 | } /* }}} */ 1046 | 1047 | void version(void) /* {{{ */ 1048 | { 1049 | fprintf(stdout, "ApacheTop %s\n", PACKAGE_VERSION); 1050 | } /* }}} */ 1051 | 1052 | void usage(void) /* {{{ */ 1053 | { 1054 | fprintf(stderr, 1055 | "ApacheTop v%s - Usage:\n" 1056 | "File options:\n" 1057 | " -f logfile open logfile (assumed common/combined) [%s]\n" 1058 | " (repeat option for more than one source)\n" 1059 | "\n" 1060 | "URL/host/referrer munging options:\n" 1061 | " -q keep query strings [%s]\n" 1062 | " -l lowercase all URLs [%s]\n" 1063 | " -s num keep num path segments of URL [all]\n" 1064 | " -p preserve protocol at front of referrers [%s]\n" 1065 | " -r resolve hostnames/IPs into each other [%s]\n" 1066 | "\n" 1067 | "Stats options:\n" 1068 | " Supply up to one of the following two. default: [-%c %d]\n" 1069 | " -H hits remember stats for this many hits\n" 1070 | " -T secs remember stats for this many seconds\n" 1071 | "\n" 1072 | " -d secs refresh delay in seconds [%d]\n" 1073 | "\n" 1074 | " -v show version\n" 1075 | " -h this help\n" 1076 | "\n" 1077 | "Compile Options: %cHAVE_KQUEUE %cHAVE_FAM %cENABLE_PCRE2\n" 1078 | "Polling Method: %s\n" 1079 | , 1080 | PACKAGE_VERSION, 1081 | DEFAULT_LOGFILE, 1082 | /* cf is set by the time we get to usage, so we can use contents */ 1083 | cf.keep_querystring ? "yes" : "no", 1084 | cf.lowercase_urls ? "yes" : "no", 1085 | cf.preserve_ref_protocol ? "yes" : "no", 1086 | cf.do_resolving ? "yes" : "no", 1087 | cf.circle_mode, cf.circle_size, 1088 | cf.refresh_delay, 1089 | #if HAVE_KQUEUE /* {{{ */ 1090 | '+', 1091 | #else 1092 | '-', 1093 | #endif /* }}} */ 1094 | #if HAVE_FAM_H /* {{{ */ 1095 | '+', 1096 | #else 1097 | '-', 1098 | #endif /* }}} */ 1099 | #if HAVE_PCRE2_H /* {{{ */ 1100 | '+', 1101 | #else 1102 | '-', 1103 | #endif /* }}} */ 1104 | (POLLING_METHOD == USING_KQUEUE ? "kqueue" : 1105 | (POLLING_METHOD == USING_FAM ? "fam" : 1106 | "stat" 1107 | ) 1108 | ) 1109 | 1110 | ); 1111 | 1112 | return; 1113 | } /* }}} */ 1114 | 1115 | int dprintf(const char *fmt, ...) /* {{{ */ 1116 | { 1117 | FILE *d; 1118 | va_list args; 1119 | static char fileName[1024] = {'\0'}; 1120 | 1121 | if ( !strlen( fileName ) ) 1122 | { 1123 | strcpy( fileName, "/tmp/atop.XXXXXX" ); 1124 | mkdtemp( fileName ); 1125 | strncat( fileName, "/debug", sizeof(fileName) - strlen(fileName) - 1 ); 1126 | } 1127 | 1128 | if (cf.debug && (d = fopen(fileName, "a"))) 1129 | { 1130 | va_start(args, fmt); 1131 | vfprintf(d, fmt, args); 1132 | fclose(d); 1133 | va_end(args); 1134 | } 1135 | 1136 | return 0; 1137 | } /* }}} */ 1138 | 1139 | static void catchsig(int s) /* {{{ */ 1140 | { 1141 | cf.exit = s; 1142 | } /* }}} */ 1143 | 1144 | /* handle a window resize by simply reopening and redrawing our window */ 1145 | static void catchwinch(int s) 1146 | { 1147 | endwin(); 1148 | refresh(); 1149 | cf.do_immed_display = true; 1150 | } 1151 | -------------------------------------------------------------------------------- /src/apachetop.h: -------------------------------------------------------------------------------- 1 | #ifndef _APACHETOP_H_ 2 | #define _APACHETOP_H_ 3 | 4 | #if HAVE_CONFIG_H 5 | # include "config.h" 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if HAVE_STRINGS_H 14 | # include 15 | #endif 16 | 17 | #include 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #if TIME_WITH_SYS_TIME 28 | # include 29 | # include 30 | #else 31 | # if HAVE_SYS_TIME_H 32 | # include 33 | # else 34 | # include 35 | # endif 36 | #endif 37 | 38 | #if HAVE_SYS_SOCKET_H 39 | # include 40 | #endif 41 | 42 | #if HAVE_NETINET_IN_H 43 | # include 44 | #endif 45 | 46 | #if HAVE_ARPA_INET_H 47 | # include 48 | #endif 49 | 50 | #if HAVE_NETDB_H 51 | # include 52 | #endif 53 | 54 | #if HAVE_PCRE2_H 55 | #define PCRE2_CODE_UNIT_WIDTH 8 56 | # include 57 | # include "pcre2_cpp_wrapper.h" 58 | #endif 59 | 60 | #include 61 | 62 | #include 63 | #include 64 | 65 | /* Use kqueue in preference to anything else. 66 | * If we don't have that, try FAM 67 | * If we don't have FAM, fall back to stat() 68 | */ 69 | #define USING_KQUEUE 1 70 | #define USING_FAM 2 71 | #define USING_STAT 3 72 | #if HAVE_KQUEUE 73 | # include 74 | # define POLLING_METHOD USING_KQUEUE 75 | #elif HAVE_FAM_H 76 | # include 77 | # define POLLING_METHOD USING_FAM 78 | #endif 79 | 80 | /* stat() fallback */ 81 | #ifndef POLLING_METHOD 82 | # define POLLING_METHOD USING_STAT 83 | #endif 84 | 85 | #if HAVE_ADNS_H 86 | # include 87 | #endif 88 | 89 | 90 | #define getMIN(a,b) (a < b ? a : b) 91 | #define getMAX(a,b) (a > b ? a : b) 92 | 93 | #ifdef HAVE_SYS_PARAM_H 94 | # include 95 | #endif 96 | 97 | /* last resort */ 98 | #ifndef MAXPATHLEN 99 | # define MAXPATHLEN 1024 100 | #endif 101 | 102 | /* upon startup, each input file is put into an element of this array, 103 | * starting at 0. The struct under this maps the current fd into this array 104 | * so we can find it without iterating. 105 | */ 106 | struct input { 107 | char *filename; 108 | int fd; 109 | ino_t inode; 110 | short type; /* types defined in log.h */ 111 | time_t lastreq; 112 | 113 | /* if open == false, then ApacheTop will periodically try to re-open 114 | * this input file. */ 115 | bool open; /* is the file open or not? */ 116 | 117 | #if (POLLING_METHOD == USING_FAM) 118 | FAMRequest famreq; 119 | #endif 120 | }; 121 | 122 | #include "filters.h" 123 | 124 | struct config { 125 | #define SORT_REQCOUNT 1 126 | #define SORT_REQPERSEC 2 127 | #define SORT_BYTECOUNT 3 128 | #define SORT_BYTESPERSEC 4 129 | #define SORT_2XX 12 /* see display.cc around line 911 (shellsort) */ 130 | #define SORT_3XX 13 /* for the consequences of changing these. */ 131 | #define SORT_4XX 14 /* Logic there relies on them being */ 132 | #define SORT_5XX 15 /* (retcode/100)+SORTTYPE_OFFSET_HACK */ 133 | #define SORTTYPE_OFFSET_HACK 10 134 | short sort, retcodes_sort; 135 | short refresh_delay; 136 | 137 | short do_resolving; 138 | 139 | short selected_item_screen; /* screen position our marker is at */ 140 | unsigned int selected_item_pos; /* which url/ip/refpos it relates to */ 141 | short selected_item_mode; /* is it url/ip/ref pos? */ 142 | unsigned int selected_item_hash; 143 | 144 | short current_display_size; /* how many lines we're displaying */ 145 | 146 | short input_count; 147 | 148 | #define TIMED_CIRCLE 'T' 149 | #define HITS_CIRCLE 'H' 150 | int circle_mode; 151 | int circle_size; 152 | 153 | /* these defines are used in cf.filter too */ 154 | #define DISPLAY_URLS 1 155 | #define DISPLAY_HOSTS 2 156 | #define DISPLAY_REFS 3 157 | #define DISPLAY_FILES 4 158 | #define DISPLAY_DETAIL 9 159 | short display_mode; 160 | 161 | #define NUMBERS_HITS_BYTES 1 162 | #define NUMBERS_RETCODES 2 163 | short numbers_mode; 164 | 165 | /* when in DISPLAY_DETAIL mode, display_mode is copied in here */ 166 | short display_mode_detail; 167 | /* what would we like to display in DISPLAY_DETAIL modes? */ 168 | bool detail_display_hosts, detail_display_refs, detail_display_urls; 169 | 170 | bool display_paused; 171 | bool do_immed_display; /* signals a screen update is req'd */ 172 | 173 | /* filters */ 174 | Filter *urlfilter, *hostfilter, *reffilter; 175 | 176 | /* url munging */ 177 | bool keep_querystring; 178 | bool lowercase_urls; 179 | unsigned short keep_segments; 180 | bool preserve_ref_protocol; 181 | 182 | bool debug; 183 | 184 | bool exit; /* true when we want to finish */ 185 | 186 | /* keypress submenu */ 187 | #define SUBMENU_NONE 0 188 | 189 | #define SUBMENU_SORT_HB 1 190 | #define SUBMENU_SORT_RC 2 191 | 192 | #define SUBMENU_DISP 4 193 | 194 | #define SUBMENU_FILT 5 195 | #define SUBMENU_FILT_ADD 6 196 | #define SUBMENU_FILT_SHOW 7 197 | 198 | #define SUBMENU_HELP 9 199 | unsigned short in_submenu; 200 | bool in_submenu_stay; /* stay in submenu till keypress? */ 201 | time_t in_submenu_time; 202 | 203 | }; 204 | 205 | struct hitinfo { 206 | double bytecount; 207 | double reqcount; 208 | time_t first, last; 209 | 210 | /* for stats worked out in display.cc */ 211 | float rps, bps; 212 | }; 213 | 214 | struct gstat { 215 | time_t start; /* when did we start */ 216 | 217 | /* space for 1xx-5xx return codes */ 218 | struct hitinfo r_codes[6]; 219 | 220 | /* space for counting global hits and bytes per sec */ 221 | struct hitinfo alltime; 222 | }; 223 | 224 | //#include "opt.h" 225 | #include "ohtbl.h" 226 | 227 | #if HAVE_ADNS_H 228 | # include "resolver.h" 229 | #endif 230 | 231 | #include "map.h" 232 | #include "circle.h" 233 | #include "hits_circle.h" 234 | #include "timed_circle.h" 235 | #include "display.h" 236 | #include "log.h" 237 | #include "queue.h" 238 | 239 | 240 | #define JAN 281 241 | #define FEB 269 242 | #define MAR 288 243 | #define APR 291 244 | #define MAY 295 245 | #define JUN 301 246 | #define JUL 299 247 | #define AUG 285 248 | #define SEP 296 249 | #define OCT 294 250 | #define NOV 307 251 | #define DEC 268 252 | 253 | #define DEBUG_OUTPUT "/tmp/atop.debug" 254 | 255 | /* this can be overridden from config.h via ./configure --with-logfile .. */ 256 | #ifndef DEFAULT_LOGFILE 257 | # define DEFAULT_LOGFILE "/var/log/access_log" 258 | #endif 259 | #define DEFAULT_CIRCLE_SIZE 30 260 | #define DEFAULT_CIRCLE_MODE TIMED_CIRCLE 261 | #define DEFAULT_SORT SORT_REQCOUNT 262 | #define DEFAULT_RETCODES_SORT SORT_2XX 263 | #define DEFAULT_REFRESH_DELAY 5 264 | #define DEFAULT_DISPLAY_MODE DISPLAY_URLS 265 | #define DEFAULT_NUMBERS_MODE NUMBERS_HITS_BYTES 266 | 267 | /* if the layout of the display changes, these need updating */ 268 | #define COLS_RESERVED 25 269 | #define LINES_RESERVED 7 270 | 271 | #define MAX_INPUT_FILES 50 272 | 273 | int recordstats(struct logbits l); 274 | 275 | int read_key(int ch); 276 | 277 | #define SEEK_TO_END true 278 | #define NO_SEEK_TO_END false 279 | int new_file(const char *filename, bool do_seek_to_end); 280 | 281 | void usage(void); 282 | void version(void); 283 | int dprintf(const char *fmt, ...); 284 | 285 | static void catchsig(int s); 286 | static void catchwinch(int s); 287 | 288 | #endif 289 | -------------------------------------------------------------------------------- /src/circle.h: -------------------------------------------------------------------------------- 1 | #ifndef _CIRCLE_H_ 2 | #define _CIRCLE_H_ 3 | 4 | class Circle 5 | { 6 | public: 7 | virtual int create(unsigned int size) = 0; 8 | 9 | virtual int insert(struct logbits lb) = 0; 10 | 11 | virtual int walk(struct logbits **lb) = 0; 12 | 13 | virtual time_t oldest(void) = 0; 14 | 15 | virtual void updatestats(void) = 0; 16 | 17 | virtual double getreqcount(void) = 0; 18 | virtual double getbytecount(void) = 0; 19 | virtual double getsummary(int r_c) = 0; 20 | }; 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /src/display.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | #include "inlines.cc" 4 | 5 | extern struct gstat gstats; 6 | extern time_t now; 7 | 8 | extern Circle *c; 9 | 10 | extern map *um, /* urlmap */ 11 | *im, /* ipmap */ 12 | *hm, /* hostmap */ 13 | *rm, /* referrermap */ 14 | *fm; /* filemap */ 15 | 16 | extern struct config cf; 17 | 18 | /* global, so we can keep it after display() finishes; the only reason for 19 | * this so far is so we know what's being displayed after this function 20 | * finishes. Hence we can translate selected_item_screen into a 21 | * selected_item_pos when the marker moves */ 22 | extern itemlist *items; 23 | extern map *last_display_map; 24 | 25 | 26 | bool display(time_t last_display) /* {{{ */ 27 | { 28 | struct gstat *w_gstats; 29 | Circle *w_c; 30 | 31 | if (cf.do_immed_display) 32 | { 33 | /* we've been asked to update immediately, whether we're 34 | * paused or not */ 35 | 36 | /* this flag enforces a clear() (why? forgot now) */ 37 | clear(); 38 | 39 | /* clear that flag */ 40 | cf.do_immed_display = false; 41 | } 42 | else if ( 43 | /* do nothing if display is paused */ 44 | cf.display_paused 45 | || 46 | /* or it's not time to update yet */ 47 | (now - last_display) < cf.refresh_delay ) 48 | return false; 49 | 50 | /* if we reach here, we're fine to update */ 51 | 52 | display_header(); 53 | 54 | //display_histogram(); 55 | //return true; 56 | 57 | if (cf.display_mode != DISPLAY_DETAIL) 58 | { 59 | /* normal display */ 60 | display_list(); 61 | 62 | return true; 63 | } 64 | 65 | /* detailed display mode involves showing the item the user is 66 | * interested in (complete stats, whether it be an URL or whatever), 67 | * followed by alternate stats. Example. The user is interested in 68 | * one particular URL, so we show that URL as it would be in 69 | * DISPLAY_URLS, then split the rest of the screen between IPs 70 | * hitting that URL, and referrers referring to that URL */ 71 | 72 | /* first line is the pos of interest */ 73 | display_list(); 74 | 75 | /* now split the screen with all other stats */ 76 | 77 | /* one line for header, one for main stat */ 78 | #define FIRST_OFFSET 2 79 | 80 | /* figure out how much room we have */ 81 | int size, cur_offset, num_sections; 82 | cur_offset = FIRST_OFFSET; 83 | 84 | switch(cf.display_mode_detail) 85 | { 86 | case DISPLAY_URLS: 87 | /* show IPs and Referrers */ 88 | num_sections = 89 | (cf.detail_display_hosts ? 1 : 0) + 90 | (cf.detail_display_refs ? 1 : 0); 91 | 92 | /* anything to do? */ 93 | if (num_sections == 0) break; 94 | 95 | size = (((LINES-LINES_RESERVED-2) - FIRST_OFFSET) / 96 | num_sections); 97 | 98 | if (cf.detail_display_hosts) 99 | { 100 | display_sub_list(DISPLAY_HOSTS,cur_offset,size); 101 | cur_offset += size + 1; 102 | } 103 | 104 | if (cf.detail_display_refs) 105 | { 106 | display_sub_list(DISPLAY_REFS,cur_offset,size); 107 | cur_offset += size + 1; 108 | } 109 | break; 110 | 111 | 112 | case DISPLAY_HOSTS: 113 | /* show URLs and Referrers */ 114 | num_sections = 115 | (cf.detail_display_urls ? 1 : 0) + 116 | (cf.detail_display_refs ? 1 : 0); 117 | 118 | /* anything to do? */ 119 | if (num_sections == 0) break; 120 | 121 | size = (((LINES-LINES_RESERVED-2) - FIRST_OFFSET) / 122 | num_sections); 123 | 124 | if (cf.detail_display_urls) 125 | { 126 | display_sub_list(DISPLAY_URLS,cur_offset,size); 127 | cur_offset += size + 1; 128 | } 129 | if (cf.detail_display_refs) 130 | { 131 | display_sub_list(DISPLAY_REFS,cur_offset,size); 132 | cur_offset += size + 1; 133 | } 134 | break; 135 | 136 | 137 | case DISPLAY_REFS: 138 | /* show URLs and IPs */ 139 | num_sections = 140 | (cf.detail_display_urls ? 1 : 0) + 141 | (cf.detail_display_hosts ? 1 : 0); 142 | 143 | /* anything to do? */ 144 | if (num_sections == 0) break; 145 | 146 | size = (((LINES-LINES_RESERVED-2) - FIRST_OFFSET) / 147 | num_sections); 148 | 149 | if (cf.detail_display_urls) 150 | { 151 | display_sub_list(DISPLAY_URLS,cur_offset,size); 152 | cur_offset += size + 1; 153 | } 154 | if (cf.detail_display_hosts) 155 | { 156 | display_sub_list(DISPLAY_HOSTS,cur_offset,size); 157 | cur_offset += size + 1; 158 | } 159 | break; 160 | } 161 | refresh(); 162 | 163 | return true; 164 | } /* }}} */ 165 | 166 | void display_header() /* {{{ */ 167 | { 168 | int itmp; 169 | float bytes, bps, per_req, ftmp; 170 | unsigned int secs_offset, diff, d = 0, h = 0, m = 0, s = 0; 171 | char bytes_suffix, bps_suffix, per_req_suffix; 172 | 173 | 174 | move(0, 0); 175 | clrtoeol(); 176 | 177 | /* last hit */ 178 | secs_offset = gstats.alltime.last % 86400; 179 | mvprintw(0, 0, "last hit: %02d:%02d:%02d", 180 | secs_offset / 3600, (secs_offset / 60) % 60, secs_offset % 60); 181 | 182 | /* uptime */ 183 | diff = (unsigned int)difftime(now, gstats.start); 184 | if (diff > 86399) diff -= ((d = diff / 86400)*86400); 185 | if (diff > 3599) diff -= ((h = diff / 3600)*3600); 186 | if (diff > 59) diff -= ((m = diff / 60)*60); 187 | s = diff; 188 | mvprintw(0, 27, "atop runtime: %2d days, %02d:%02d:%02d", d, h, m, s); 189 | 190 | /* are we paused? */ 191 | if (cf.display_paused) 192 | { 193 | DRAW_PAUSED(0,60); /* macro in display.h */ 194 | } 195 | 196 | /* current time */ 197 | secs_offset = now % 86400; 198 | mvprintw(0, 71, "%02d:%02d:%02d", 199 | secs_offset /3600, (secs_offset/ 60) % 60, secs_offset % 60); 200 | 201 | 202 | //All: 1,140,532 requests (39.45/sec), 999,540,593 bytes (857,235/sec) 203 | ftmp = getMAX(now-gstats.alltime.first, 1); /* divide-by-zero hack */ 204 | bytes = readableNum(gstats.alltime.bytecount, &bytes_suffix); 205 | bps = readableNum(gstats.alltime.bytecount/ftmp, &bps_suffix); 206 | per_req = readableNum( 207 | gstats.alltime.bytecount/getMAX(gstats.alltime.reqcount, 1), 208 | &per_req_suffix); 209 | attron(A_BOLD); 210 | mvprintw(1, 0, 211 | "All: %12.0f reqs (%6.1f/sec) %11.1f%c (%7.1f%c/sec) %7.1f%c/req", 212 | gstats.alltime.reqcount, 213 | gstats.alltime.reqcount/ftmp, 214 | bytes, bytes_suffix, 215 | bps, bps_suffix, 216 | per_req, per_req_suffix); 217 | attroff(A_BOLD); 218 | 219 | 220 | // 2xx 1,604,104 (95%) 3xx 1,000,000 ( 3%) 4xx 1,000,000 ( 1%) 221 | // 5xx 1,000,000 ( 1%) 222 | ftmp = gstats.r_codes[2].reqcount + gstats.r_codes[3].reqcount+ 223 | gstats.r_codes[4].reqcount + gstats.r_codes[5].reqcount; 224 | if (ftmp == 0) ftmp = 1; /* avoid NaN with no hits */ 225 | mvprintw(2, 0, 226 | "2xx: %7.0f (%4.*f%%) 3xx: %7.0f (%4.*f%%) " 227 | "4xx: %5.0f (%4.*f%%) 5xx: %5.0f (%4.*f%%) ", 228 | 229 | gstats.r_codes[2].reqcount, 230 | (gstats.r_codes[2].reqcount/ftmp) == 1 ? 0 : 1, 231 | (gstats.r_codes[2].reqcount/ftmp)*100, 232 | 233 | gstats.r_codes[3].reqcount, 234 | (gstats.r_codes[3].reqcount/ftmp) == 1 ? 0 : 1, 235 | (gstats.r_codes[3].reqcount/ftmp)*100, 236 | 237 | gstats.r_codes[4].reqcount, 238 | (gstats.r_codes[4].reqcount/ftmp) == 1 ? 0 : 1, 239 | (gstats.r_codes[4].reqcount/ftmp)*100, 240 | 241 | gstats.r_codes[5].reqcount, 242 | (gstats.r_codes[5].reqcount/ftmp) == 1 ? 0 : 1, 243 | (gstats.r_codes[5].reqcount/ftmp)*100 244 | 245 | ); 246 | 247 | /* housecleaning on the circle, if its required in this class */ 248 | c->updatestats(); 249 | /* fetch the time of the first "recent" request */ 250 | itmp = now - c->oldest(); 251 | itmp = getMAX(itmp, 1); /* divide-by-zero hack */ 252 | bytes = readableNum(c->getbytecount(), &bytes_suffix); 253 | bps = readableNum(c->getbytecount()/itmp, &bps_suffix); 254 | per_req = readableNum(c->getbytecount()/getMAX(c->getreqcount(), 1), 255 | &per_req_suffix); 256 | attron(A_BOLD); 257 | mvprintw(3, 0, 258 | "R (%3ds): %7.0f reqs (%6.1f/sec) %11.1f%c (%7.1f%c/sec) %7.1f%c/req", 259 | itmp, c->getreqcount(), 260 | ((float)c->getreqcount()/itmp), 261 | bytes, bytes_suffix, 262 | bps, bps_suffix, 263 | per_req, per_req_suffix 264 | ); 265 | attroff(A_BOLD); 266 | 267 | ftmp = c->getsummary(2) + c->getsummary(3) + 268 | c->getsummary(4) + c->getsummary(5); 269 | if (ftmp == 0) ftmp = 1; /* avoid NaN with no hits */ 270 | mvprintw(4, 0, 271 | "2xx: %7.0f (%4.*f%%) 3xx: %7.0f (%4.*f%%) " 272 | "4xx: %5.0f (%4.*f%%) 5xx: %5.0f (%4.*f%%) ", 273 | c->getsummary(2), 274 | (c->getsummary(2)/ftmp) == 1 ? 0 : 1, 275 | (c->getsummary(2)/ftmp)*100, 276 | 277 | c->getsummary(3), 278 | (c->getsummary(3)/ftmp) == 1 ? 0 : 1, 279 | (c->getsummary(3)/ftmp)*100, 280 | 281 | c->getsummary(4), 282 | (c->getsummary(4)/ftmp) == 1 ? 0 : 1, 283 | (c->getsummary(4)/ftmp)*100, 284 | 285 | c->getsummary(5), 286 | (c->getsummary(5)/ftmp) == 1 ? 0 : 1, 287 | (c->getsummary(5)/ftmp)*100 288 | ); 289 | 290 | // mvprintw(5, 0, 291 | // "Unique Objects: Size Footprint:"); 292 | 293 | /* if any filters are active, and the user is not in a submenu, 294 | * display a summary */ 295 | if (cf.in_submenu == SUBMENU_NONE) 296 | { 297 | int y; 298 | char active_filters[20]; 299 | bzero(active_filters, sizeof(active_filters)); 300 | 301 | /* suss out which filters are active */ 302 | if (cf.urlfilter->isactive()) 303 | strcat(active_filters, "URLs "); 304 | if (cf.hostfilter->isactive()) 305 | strcat(active_filters, "HOSTs "); 306 | if (cf.reffilter->isactive()) 307 | strcat(active_filters, "REFs "); 308 | 309 | if (*active_filters) 310 | { 311 | y = COLS - (strlen(active_filters) + 11); 312 | mvprintw(SUBMENU_LINE_NUMBER, 313 | y, "Filtering: %s", active_filters); 314 | } 315 | 316 | } 317 | 318 | } /* }}} */ 319 | 320 | void display_list() /* {{{ */ 321 | { 322 | int x, t, xx; 323 | char *cptmp; 324 | struct config scf; /* for copying cf, see comment below at memcpy */ 325 | 326 | OAHash item_hash; 327 | int item_used = 0, disp; 328 | double items_size; 329 | struct itemlist *item_ptr = NULL; 330 | 331 | /* for easy reference during walk() */ 332 | map *map; unsigned int hash; 333 | int pos; 334 | 335 | /* walk() pointer */ 336 | struct logbits *lb; 337 | 338 | /* make an array containing all we have, then sort it */ 339 | items_size = c->getreqcount(); 340 | if (items_size == 0) 341 | { 342 | /* nothing to do! */ 343 | move(LINES_RESERVED-1, 0); 344 | clrtobot(); 345 | refresh(); 346 | return; 347 | } 348 | 349 | item_hash.create( (int)items_size * 5 ); 350 | 351 | /* this is our array; the cast is because items_size is a double but 352 | * I'm fairly sure, realistically, it'll never get high enough to be 353 | * a problem, so uInt should be ok */ 354 | if (items) free(items); /* get rid of the last one */ 355 | items = (struct itemlist *) 356 | calloc((unsigned int)items_size, sizeof(itemlist)); 357 | 358 | /* another thread may change the contents of cf while we're running, 359 | * and it would be undesirable to have most of this change on us, so 360 | * we make safe copies to use */ 361 | memcpy(&scf, &cf, sizeof(struct config)); 362 | 363 | /* if we are in detailed display mode, then we have to overwrite 364 | * scf.display_mode so all these switch()'es work */ 365 | if (scf.display_mode == DISPLAY_DETAIL) 366 | scf.display_mode = cf.display_mode_detail; 367 | 368 | /* pick a map to use */ 369 | switch(scf.display_mode) 370 | { 371 | default: 372 | case DISPLAY_URLS: map = um; break; 373 | case DISPLAY_HOSTS: map = hm; break; 374 | case DISPLAY_REFS: map = rm; break; 375 | case DISPLAY_FILES: map = fm; break; 376 | } 377 | 378 | /* store that in a global that we can remember */ 379 | last_display_map = map; 380 | 381 | /* walk the entire circle */ 382 | while(c->walk(&lb) != -1) 383 | { 384 | /* skip unused */ 385 | if (lb == NULL) 386 | continue; 387 | 388 | 389 | /* set up some pointers, depending on cf.display_mode, so 390 | * that we can refer to the url_pos or ip_pos via just pos, 391 | * and the urlmap or ipmap via just map 392 | */ 393 | switch(scf.display_mode) 394 | { 395 | default: 396 | case DISPLAY_URLS: 397 | hash = lb->url_hash; pos = lb->url_pos; 398 | break; 399 | 400 | case DISPLAY_HOSTS: 401 | hash = lb->host_hash; pos = lb->host_pos; 402 | break; 403 | 404 | case DISPLAY_REFS: 405 | hash = lb->ref_hash; pos = lb->ref_pos; 406 | break; 407 | #if HAVE_FILE_MODE_DISPLAY 408 | case DISPLAY_FILES: 409 | hash = lb->file_hash ; pos = lb->file_pos; 410 | break; 411 | #endif 412 | } 413 | 414 | /* FILTERS {{{ */ 415 | /* skip this item if it doesn't match our filter */ 416 | if (cf.urlfilter->isactive() && 417 | !cf.urlfilter->match(um->reverse(lb->url_pos))) 418 | continue; 419 | 420 | if (cf.hostfilter->isactive() && 421 | !cf.hostfilter->match(hm->reverse(lb->host_pos))) 422 | continue; 423 | 424 | if (cf.reffilter->isactive() && 425 | !cf.reffilter->match(rm->reverse(lb->ref_pos))) 426 | continue; 427 | /* }}} */ 428 | 429 | /* look up whatever string this pos is for */ 430 | cptmp = map->reverse(pos); 431 | 432 | /* then lookup that string in items */ 433 | item_ptr = (struct itemlist *)item_hash.lookup(cptmp); 434 | 435 | 436 | /* do we have this string already? */ 437 | if (item_ptr == NULL) 438 | { 439 | /* not seen it, make a new slot */ 440 | item_ptr = &items[item_used]; 441 | 442 | item_hash.insert(cptmp, item_ptr); 443 | item_ptr->item = pos; 444 | 445 | /* store the ip position in the itemlist too, just 446 | ** in case we have to look it up in show_map_line 447 | */ 448 | item_ptr->ip_item = lb->ip_pos; 449 | 450 | item_ptr->hash = hash; 451 | 452 | ++item_used; 453 | item_ptr->last = now; 454 | item_ptr->first = lb->time; 455 | } 456 | 457 | /* we have the string in items now, so update stats in array */ 458 | item_ptr->reqcount++; 459 | item_ptr->bytecount += lb->bytes; 460 | 461 | /* we wish to count up how many times a given retcode 462 | * has occurred for this item */ 463 | t = (int)(lb->retcode/100); 464 | item_ptr->r_codes[t].reqcount++; 465 | item_ptr->r_codes[t].bytecount += lb->bytes; 466 | 467 | if (lb->time < item_ptr->first) 468 | item_ptr->first = lb->time; 469 | } 470 | 471 | /* no further use for this hash */ 472 | item_hash.destroy(); 473 | 474 | /* calculate some stats; timespan, kbps, rps */ 475 | for(x = 0 ; x < item_used ; ++x) 476 | { 477 | item_ptr = &items[x]; 478 | 479 | item_ptr->timespan = item_ptr->last - item_ptr->first; 480 | if (item_ptr->timespan == 0) 481 | item_ptr->timespan = 1; /* hack to avoid /0 */ 482 | 483 | for(xx = 2 ; xx < 6 ; xx++) 484 | { 485 | item_ptr->r_codes[xx].rps = ((float) 486 | item_ptr->r_codes[xx].reqcount/item_ptr->timespan); 487 | 488 | item_ptr->r_codes[xx].bps = ((float) 489 | item_ptr->r_codes[xx].bytecount/item_ptr->timespan); 490 | } 491 | 492 | item_ptr->kbps = ((float) 493 | (item_ptr->bytecount/1024)/item_ptr->timespan); 494 | item_ptr->rps = ((float) 495 | item_ptr->reqcount/item_ptr->timespan); 496 | } 497 | 498 | move(SUBMENU_LINE_NUMBER+1, 0); 499 | clrtobot(); 500 | 501 | /* a suitable header, at LINES_RESERVED-1 502 | * (-1 because curses starts at zero, but I've started at 1) */ 503 | switch(scf.numbers_mode) 504 | { 505 | case NUMBERS_HITS_BYTES: 506 | mvaddstr(LINES_RESERVED-1, 0, " REQS REQ/S KB KB/S"); 507 | break; 508 | 509 | case NUMBERS_RETCODES: 510 | mvaddstr(LINES_RESERVED-1, 0, " 2xx 3xx 4xx 5xx"); 511 | break; 512 | } 513 | 514 | /* what are we showing in the table? we have a few options; 515 | * 516 | * top $X URLS or IPs/Hosts or Referrers 517 | * detailed referrer stats for a given URL 518 | */ 519 | if (cf.display_mode == DISPLAY_DETAIL) 520 | { 521 | /* detailed referrer stats for a given URL */ 522 | // mvaddstr(LINES_RESERVED-1, 0, "REQS REQ/S KB KB/S"); 523 | 524 | /* display only the item we're interested in */ 525 | for (x = 0 ; x < item_used ; ++x) 526 | { 527 | item_ptr = &items[x]; 528 | if (item_ptr->hash == cf.selected_item_hash) 529 | { 530 | /* show stats for this line only */ 531 | show_map_line(item_ptr, 0, map, 532 | NO_INDENT, scf.numbers_mode); 533 | break; 534 | } 535 | } 536 | /* and return, display_sub_list can do the rest */ 537 | return; 538 | } 539 | 540 | /* top $X items; sort the array for display */ 541 | if (item_used) shellsort_wrapper(items, item_used, scf); 542 | 543 | /* display something */ 544 | 545 | switch(scf.display_mode) 546 | { 547 | case DISPLAY_URLS: 548 | mvaddstr(LINES_RESERVED-1, 23, "URL"); 549 | break; 550 | 551 | case DISPLAY_HOSTS: 552 | mvaddstr(LINES_RESERVED-1, 23, "HOST"); 553 | break; 554 | 555 | case DISPLAY_REFS: 556 | mvaddstr(LINES_RESERVED-1, 23, "REFERRER"); 557 | break; 558 | } 559 | 560 | disp = 0; /* count how many we've shown */ 561 | for(x = 0, item_ptr = &items[0] ; 562 | x < item_used && disp < LINES-LINES_RESERVED-1 ; 563 | ++item_ptr, ++x) 564 | { 565 | /* skip empty tablespaces, even though none should exist */ 566 | if (item_ptr->reqcount == 0) 567 | continue; 568 | 569 | /* render the line itself at position disp. map should be 570 | * already set from earlier in this function */ 571 | show_map_line(item_ptr, disp, map, 572 | NO_INDENT, scf.numbers_mode); 573 | 574 | ++disp; 575 | } 576 | 577 | /* translate the screen position (cf.selected_item_screen) into an 578 | * url/ip/ref_pos (cf.selected_item_pos), depending on current 579 | * display mode. We can then use selected_item_pos to get info for 580 | * the selected item if the user hits return to get it */ 581 | translate_screen_to_pos(); 582 | 583 | #if 0 584 | /* debug line */ 585 | mvprintw(5,0, "%s", map->reverse(cf.selected_item_pos)); 586 | #endif 587 | 588 | cf.current_display_size = disp; 589 | 590 | /* if there were items, draw a marker */ 591 | if (cf.current_display_size) 592 | drawMarker(); 593 | else 594 | { 595 | /* come to rest just above the column headers */ 596 | move(LINES_RESERVED-2,0); 597 | refresh(); 598 | } 599 | } /* }}} */ 600 | 601 | void display_sub_list(short display_mode_override, /* {{{ */ 602 | unsigned short offset, unsigned short limit) 603 | { 604 | /* similar to display_list(), but this one just uses a section of 605 | * the screen for displaying information pertinent to a particular 606 | * URL or IP or REFERRER. 607 | */ 608 | 609 | /* I'd like to lose this code and replace the breakdown screen 610 | * with something more useful.. */ 611 | 612 | int x, t, xx; 613 | char *cptmp; 614 | unsigned int h; 615 | struct config scf; /* for copying cf, see comment below at memcpy */ 616 | 617 | OAHash item_hash; 618 | int item_used = 0, disp; 619 | double items_size; 620 | struct itemlist *subitems, *item_ptr = NULL; 621 | 622 | /* for easy reference during walk() */ 623 | map *map; unsigned int pos; 624 | 625 | /* walk() pointer */ 626 | struct logbits *lb; 627 | 628 | if (c->getreqcount() == 0) 629 | { 630 | /* nothing to do! */ 631 | refresh(); 632 | return; 633 | } 634 | 635 | /* make an array containing all we have, then sort it */ 636 | items_size = c->getreqcount(); 637 | item_hash.create( (int)items_size * 5 ); 638 | 639 | /* this is our array; the cast is because items_size is a double but 640 | * I'm fairly sure, realistically, it'll never get high enough to be 641 | * a problem, so uInt should be ok */ 642 | //if (items) free(items); /* get rid of the last one */ 643 | subitems = (struct itemlist *) 644 | calloc((unsigned int)items_size, sizeof(itemlist)); 645 | 646 | /* another thread may change the contents of cf while we're running, 647 | * and it would be undesirable to have most of this change on us, so 648 | * we make safe copies to use */ 649 | memcpy(&scf, &cf, sizeof(struct config)); 650 | 651 | /* pick a map to use */ 652 | switch(display_mode_override) 653 | { 654 | default: 655 | case DISPLAY_URLS: map = um; break; 656 | case DISPLAY_HOSTS: map = hm; break; 657 | case DISPLAY_REFS: map = rm; break; 658 | } 659 | 660 | /* walk the entire circle */ 661 | while(c->walk(&lb) != -1) 662 | { 663 | /* skip unused */ 664 | if (lb == NULL) 665 | continue; 666 | 667 | /* FILTERS? */ 668 | 669 | 670 | /* set up some pointers, depending on cf.display_mode, so 671 | * that we can refer to the url_pos or ip_pos via just pos, 672 | * and the urlmap or ipmap via just map 673 | */ 674 | switch(scf.display_mode_detail) 675 | { 676 | default: 677 | case DISPLAY_URLS: h = lb->url_hash; break; 678 | case DISPLAY_HOSTS: h = lb->host_hash; break; 679 | case DISPLAY_REFS: h = lb->ref_hash; break; 680 | } 681 | 682 | /* we're only interested in this circle item if it matches 683 | * the url/ip/referrer we're interested in; remember this is 684 | * a sub-list pertinent to one particular master item */ 685 | if (h != cf.selected_item_hash) 686 | continue; 687 | 688 | switch(display_mode_override) 689 | { 690 | default: 691 | case DISPLAY_URLS: pos = lb->url_pos; break; 692 | case DISPLAY_HOSTS: pos = lb->host_pos; break; 693 | case DISPLAY_REFS: pos = lb->ref_pos; break; 694 | } 695 | 696 | /* look up whatever string this pos is for */ 697 | cptmp = map->reverse(pos); 698 | 699 | /* then lookup that string in items */ 700 | item_ptr = (struct itemlist *)item_hash.lookup(cptmp); 701 | 702 | /* do we have this string already? */ 703 | if (item_ptr == NULL) 704 | { 705 | /* not seen it, make a new slot */ 706 | item_ptr = &subitems[item_used]; 707 | 708 | item_hash.insert(cptmp, item_ptr); 709 | item_ptr->item = pos; 710 | 711 | /* store the ip position in the itemlist too, just 712 | ** in case we have to look it up in show_map_line 713 | */ 714 | item_ptr->ip_item = lb->ip_pos; 715 | 716 | item_ptr->last = now; 717 | item_ptr->first = lb->time; 718 | 719 | ++item_used; 720 | } 721 | 722 | /* we have the string in items now, so update stats in array */ 723 | ++(item_ptr->reqcount); 724 | item_ptr->bytecount += lb->bytes; 725 | 726 | /* we wish to count up how many times a given retcode 727 | * has occurred for this item */ 728 | t = (int)(lb->retcode/100); 729 | item_ptr->r_codes[t].reqcount++; 730 | item_ptr->r_codes[t].bytecount += lb->bytes; 731 | 732 | if (lb->time < item_ptr->first) 733 | item_ptr->first = lb->time; 734 | } 735 | 736 | /* no further use for this lookup hash */ 737 | item_hash.destroy(); 738 | 739 | /* calculate some stats; timespan, kbps, rps */ 740 | for(x = 0 ; x < item_used ; ++x) 741 | { 742 | item_ptr = &subitems[x]; 743 | 744 | item_ptr->timespan = item_ptr->last - item_ptr->first; 745 | if (item_ptr->timespan == 0) 746 | item_ptr->timespan = 1; /* hack to avoid /0 */ 747 | 748 | for(xx = 2 ; xx < 6 ; xx++) 749 | { 750 | item_ptr->r_codes[xx].rps = ((float) 751 | item_ptr->r_codes[xx].reqcount/item_ptr->timespan); 752 | 753 | item_ptr->r_codes[xx].bps = ((float) 754 | item_ptr->r_codes[xx].bytecount/item_ptr->timespan); 755 | } 756 | 757 | item_ptr->kbps = ((float) 758 | (item_ptr->bytecount/1024)/item_ptr->timespan); 759 | item_ptr->rps = ((float) 760 | item_ptr->reqcount/item_ptr->timespan); 761 | } 762 | 763 | /* top $X items; sort the array for display */ 764 | if (item_used) shellsort_wrapper(subitems, item_used, scf); 765 | 766 | 767 | //clrtobot(); 768 | 769 | /* display something */ 770 | 771 | /* a suitable header, at offset 772 | * (-1 because curses starts at zero, but I've started at 1) */ 773 | switch(display_mode_override) 774 | { 775 | case DISPLAY_URLS: 776 | mvaddstr(LINES_RESERVED+offset-1, 23, "URL"); 777 | break; 778 | 779 | case DISPLAY_HOSTS: 780 | mvaddstr(LINES_RESERVED+offset-1, 23, "HOST"); 781 | break; 782 | 783 | case DISPLAY_REFS: 784 | mvaddstr(LINES_RESERVED+offset-1, 23, "REFERRER"); 785 | break; 786 | } 787 | 788 | disp = offset; /* count how many we've shown */ 789 | for(x = 0, item_ptr = &subitems[0] ; 790 | x < items_size && disp < limit+offset ; ++item_ptr, ++x) 791 | { 792 | /* skip empty tablespaces, even though none should exist */ 793 | if (item_ptr->reqcount == 0) 794 | continue; 795 | 796 | /* render the line itself at position disp. map should be 797 | * already set from earlier in this function */ 798 | show_map_line(item_ptr, disp, map, 2, scf.numbers_mode); 799 | 800 | ++disp; 801 | } 802 | free(subitems); 803 | } /* }}} */ 804 | 805 | void translate_screen_to_pos() /* {{{ */ 806 | { 807 | /* don't do anything if there's nothing on screen */ 808 | if (items == NULL) return; 809 | 810 | /* convert cf.selected_item_screen to cf.selected_item_pos */ 811 | cf.selected_item_pos = items[cf.selected_item_screen].item; 812 | 813 | // /* cf.selected_item_pos may well be zero */ 814 | // if (cf.selected_item_pos) 815 | // { 816 | /* make a hash of it */ 817 | cf.selected_item_hash = 818 | TTHash(last_display_map->reverse(cf.selected_item_pos)); 819 | 820 | cf.selected_item_mode = cf.display_mode; 821 | // } 822 | } /* }}} */ 823 | 824 | void drawMarker(void) /* update position of asterisk next to URLs {{{ */ 825 | { 826 | if (cf.current_display_size == 0) 827 | return; 828 | 829 | /* ensure our marker isn't beyond the end of the list */ 830 | if (cf.selected_item_screen > cf.current_display_size-1) 831 | cf.selected_item_screen = cf.current_display_size-1; 832 | 833 | /* or above the start of it */ 834 | if (cf.selected_item_screen < 0) cf.selected_item_screen = 0; 835 | 836 | /* draw an asterisk next to the selected item and clear adjacent lines*/ 837 | mvaddch(LINES_RESERVED+cf.selected_item_screen-1, COLS_RESERVED-3, ' '); 838 | mvaddch(LINES_RESERVED+cf.selected_item_screen, 839 | COLS_RESERVED-3, '*' | A_BOLD); 840 | mvaddch(LINES_RESERVED+cf.selected_item_screen+1, COLS_RESERVED-3, ' '); 841 | 842 | /* come to rest under header */ 843 | move(SUBMENU_LINE_NUMBER, 0); 844 | 845 | refresh(); 846 | } /* }}} */ 847 | 848 | 849 | /* render contents of item_ptr into a statistics line onscreen, 850 | * at offset vert_location; pull the text portion out of m */ 851 | void show_map_line(struct itemlist *item_ptr, int vert_location, /* {{{ */ 852 | map *m, unsigned short indent, unsigned short number_mode) 853 | { 854 | if (number_mode == NUMBERS_HITS_BYTES) 855 | { 856 | /* start drawing at LINES_RESERVED */ 857 | mvprintw(LINES_RESERVED+vert_location, 0, 858 | "%5.0f %5.*f %5.*f %4.*f", 859 | item_ptr->reqcount, 860 | 861 | /* set 1dp if rps > 99, or 2 if not */ 862 | ((item_ptr->rps > 99) ? 1 : 2), 863 | item_ptr->rps, 864 | 865 | /* scale KB display; if >1000K lose the decimal point */ 866 | ((item_ptr->bytecount > (999*1024)) ? 0 : 1), 867 | ((float)item_ptr->bytecount/1024), 868 | 869 | /* scale KB/s display; if >1000K/s lose the decimal point */ 870 | ((item_ptr->kbps > 99) ? 0 : 1), 871 | item_ptr->kbps); 872 | } 873 | else /* number_mode == NUMBERS_RETCODES */ 874 | { 875 | mvprintw(LINES_RESERVED+vert_location, 0, 876 | "%5.0f %5.0f %5.0f %4.0f", 877 | item_ptr->r_codes[2].reqcount, 878 | item_ptr->r_codes[3].reqcount, 879 | item_ptr->r_codes[4].reqcount, 880 | item_ptr->r_codes[5].reqcount); 881 | } 882 | 883 | /* text after the number columns */ 884 | 885 | /* if we are displaying host/ip rather than anything else, we 886 | ** have slightly more work to do; we need to generate a line of the 887 | ** format host* [ip] 888 | ** * only needs to be present if it's still resolving. 889 | ** ip only needs to be present if list_show_ip is true. 890 | */ 891 | 892 | /* 23+indent = start at 23, but move to the right by indent 893 | ** spaces if we are doing, for example, a sublist 894 | */ 895 | if (m == hm) 896 | { 897 | /* we are displaying a host line */ 898 | char *h = NULL, *i = NULL; 899 | int host_item, ip_item; 900 | 901 | /* 200 chars ought to be enough? */ 902 | #define MAX_IP_STR_WIDTH 200 903 | 904 | char str[MAX_IP_STR_WIDTH]; 905 | 906 | host_item = item_ptr->item; 907 | ip_item = item_ptr->ip_item; 908 | 909 | if (host_item >= 0) h = m->reverse(host_item); 910 | if (ip_item >= 0) i = im->reverse(ip_item); 911 | 912 | if (h && i) 913 | snprintf(str, MAX_IP_STR_WIDTH, "%s [%s]", h, i); 914 | else if (h) 915 | snprintf(str, MAX_IP_STR_WIDTH, "%s", h); 916 | else if (i) 917 | snprintf(str, MAX_IP_STR_WIDTH, "[%s]", i); 918 | else 919 | return; /* shouldn't get reached */ 920 | 921 | mvprintw(LINES_RESERVED+vert_location, 23+indent, "%.*s", 922 | (COLS-COLS_RESERVED /* width */), str); 923 | } 924 | else 925 | { 926 | char *str = m->reverse(item_ptr->item); 927 | mvprintw(LINES_RESERVED+vert_location, 23+indent, "%.*s", 928 | (COLS-COLS_RESERVED /* width */), str); 929 | } 930 | 931 | 932 | } /* }}} */ 933 | 934 | void shellsort_wrapper(struct itemlist *items, unsigned int size, /* {{{ */ 935 | struct config pcf) 936 | { 937 | short sort_method; 938 | 939 | if (size == 0) return; 940 | 941 | /* what are we displaying in the numbers column? */ 942 | switch(pcf.numbers_mode) 943 | { 944 | /* sorting by hits or bytes */ 945 | case NUMBERS_HITS_BYTES: 946 | sort_method = pcf.sort; 947 | break; 948 | 949 | /* sorting by a return code */ 950 | case NUMBERS_RETCODES: 951 | sort_method = pcf.retcodes_sort; 952 | break; 953 | } 954 | 955 | shellsort(items, size, sort_method); 956 | } /* }}} */ 957 | void shellsort(struct itemlist *items, unsigned int size, int sorttype) /* {{{ */ 958 | { 959 | int x; 960 | bool done; 961 | unsigned int i, j, jmp = size; 962 | double i_c, j_c; 963 | 964 | struct itemlist tmp; 965 | 966 | while (jmp > 1) 967 | { 968 | jmp >>= 1; 969 | do 970 | { 971 | done = true; 972 | for (j = 0; j < (size-jmp); j++) 973 | { 974 | i = j + jmp; 975 | if (cf.numbers_mode == NUMBERS_HITS_BYTES) 976 | { 977 | /* numbers_mode = NUMBERS_HITS_BYTES */ 978 | switch(sorttype) 979 | { 980 | default: 981 | case SORT_REQCOUNT: 982 | i_c = items[i].reqcount; 983 | j_c = items[j].reqcount; 984 | break; 985 | 986 | case SORT_REQPERSEC: 987 | i_c = items[i].rps; 988 | j_c = items[j].rps; 989 | break; 990 | 991 | case SORT_BYTECOUNT: 992 | i_c = items[i].bytecount; 993 | j_c = items[j].bytecount; 994 | break; 995 | 996 | case SORT_BYTESPERSEC: 997 | i_c = items[i].kbps; 998 | j_c = items[j].kbps; 999 | break; 1000 | } 1001 | } 1002 | else 1003 | { 1004 | /* numbers_mode = NUMBERS_RETCODES */ 1005 | /* this is easier ;) 1006 | * see apachetop.h for explanation 1007 | * of what SORTTYPE_OFFSET_HACK 1008 | * is */ 1009 | x = sorttype-SORTTYPE_OFFSET_HACK; 1010 | i_c = items[i].r_codes[x].reqcount; 1011 | j_c = items[j].r_codes[x].reqcount; 1012 | } 1013 | 1014 | if (i_c > j_c) 1015 | { 1016 | #define P_S_S sizeof(struct itemlist) 1017 | memcpy(&tmp, &items[i], P_S_S); 1018 | memcpy(&items[i], &items[j], P_S_S); 1019 | memcpy(&items[j], &tmp, P_S_S); 1020 | 1021 | done = false; 1022 | } 1023 | } 1024 | } while (!done); 1025 | } 1026 | } /* }}} */ 1027 | 1028 | float readableNum(double num, char *suffix) /* {{{ */ 1029 | { 1030 | #define AP_TEN_KB ((double)(1024)*10) 1031 | #define AP_TEN_MB ((double)(1024*1024)*10) 1032 | #define AP_TEN_GB ((double)(1024*1024*1024)*10) 1033 | 1034 | if (num > AP_TEN_GB) 1035 | { 1036 | *suffix = 'G'; 1037 | return (float)num/((double)(1024*1024*1024)); 1038 | } 1039 | if (num > AP_TEN_MB) 1040 | { 1041 | *suffix = 'M'; 1042 | return (float)num/((double)(1024*1024)); 1043 | } 1044 | if (num > AP_TEN_KB) 1045 | { 1046 | *suffix = 'K'; 1047 | return (float)num/1024; 1048 | } 1049 | 1050 | *suffix = 'B'; 1051 | return (float)num; 1052 | } /* }}} */ 1053 | 1054 | void display_submenu_banner(const char *title, int title_len, const char *banner) 1055 | { 1056 | attron(A_REVERSE); 1057 | mvaddstr(SUBMENU_LINE_NUMBER, 1, title); 1058 | attroff(A_REVERSE); 1059 | mvaddstr(SUBMENU_LINE_NUMBER, 1+title_len, banner); 1060 | move(SUBMENU_LINE_NUMBER, 0); 1061 | refresh(); 1062 | } 1063 | 1064 | void clear_submenu_banner(void) 1065 | { 1066 | /* remove submenu banner */ 1067 | move(SUBMENU_LINE_NUMBER, 0); 1068 | clrtoeol(); 1069 | refresh(); 1070 | } 1071 | 1072 | void display_help(void) 1073 | { 1074 | clear(); 1075 | 1076 | move(0, 0); 1077 | printw("ApacheTop version %s", PACKAGE_VERSION); 1078 | move(0, 27); 1079 | addstr("Copyright (c) 2003-2011 Chris Elsworth"); 1080 | move(1, 27); 1081 | addstr("Copyright (c) 2015- Helmut K. C. Tessarek"); 1082 | 1083 | move(3, 0); 1084 | addstr("ONE-TOUCH COMMANDS\n"); 1085 | addstr("d : switch item display between urls/referrers/hosts\n"); 1086 | addstr("n : switch numbers display between hits & bytes or return codes\n"); 1087 | addstr("h or ? : this help window\n"); 1088 | addstr("p : (un)pause display (freeze updates)\n"); 1089 | addstr("q : quit ApacheTop\n"); 1090 | addstr("up/down : move marker asterisk up/down\n"); 1091 | addstr("right/left : enter/exit detailed subdisplay mode\n"); 1092 | addstr("\n"); 1093 | addstr("SUBMENUS:\n"); 1094 | addstr("s: SORT BY: [the appropriate menu will appear for your display]\n"); 1095 | addstr("\tr) requests R) reqs/sec b) bytes B) bytes/sec\n"); 1096 | addstr("\t2) 2xx 3) 3xx 4) 4xx 5) 5xx\n\n"); 1097 | addstr("t: TOGGLE SUBDISPLAYS ON/OFF:\n"); 1098 | addstr("\tu) urls r) referrers h) hosts\n\n"); 1099 | addstr("f: MANIPULATE FILTERS:\n"); 1100 | addstr("\ta) add/edit menu c) clear all s) show active (not done yet)\n"); 1101 | addstr("\ta: ADD FILTER SUBMENU\n"); 1102 | addstr("\t\tu) to urls r) to referrers h) to hosts\n"); 1103 | addstr("\n"); 1104 | attron(A_REVERSE); 1105 | addstr("Hit any key to continue:"); 1106 | attroff(A_REVERSE); 1107 | 1108 | refresh(); 1109 | cf.display_paused = true; 1110 | } 1111 | 1112 | void display_active_filters() 1113 | { 1114 | clear(); 1115 | 1116 | move(0, 0); 1117 | 1118 | addstr("ApacheTop: Currently Active Filters\n"); 1119 | 1120 | 1121 | addstr("\n"); 1122 | attron(A_REVERSE); 1123 | addstr("Hit any key to continue:"); 1124 | attroff(A_REVERSE); 1125 | 1126 | refresh(); 1127 | cf.display_paused = true; 1128 | 1129 | } 1130 | 1131 | void display_histogram() 1132 | { 1133 | int hist_height, hist_width; 1134 | int i, j, age, oldest; 1135 | //int y_scale; 1136 | float y_scale, y_decr; 1137 | float x_scale, max_bar = 0; 1138 | char horiz_line[128]; 1139 | 1140 | struct logbits *lb; 1141 | 1142 | /* histogram starts at LINES_RESERVED+10 */ 1143 | #define HISTOGRAM_START LINES_RESERVED+3 1144 | hist_height = 10; 1145 | hist_width = 60; 1146 | 1147 | /* for every width character, figure out how many 1148 | * characters high to draw the barchart 1149 | */ 1150 | float bar_height[hist_width]; 1151 | char line[hist_width + 1]; 1152 | for(i = 0 ; i < hist_width ; i++) 1153 | bar_height[i] = 0; 1154 | 1155 | /* figure out scales; includes divide-by-zero avoidance hack */ 1156 | if (cf.circle_mode == TIMED_CIRCLE) 1157 | /* timed_circle = we know exactly how old we're going to get */ 1158 | x_scale = ((float)hist_width/cf.circle_size); 1159 | else 1160 | x_scale = ((float)hist_width/getMAX(now - c->oldest(), 1)); 1161 | 1162 | /* don't scale when we have less data than we have room for */ 1163 | if (x_scale > 1) x_scale = 1; 1164 | 1165 | while(c->walk(&lb) != -1) 1166 | { 1167 | if (!lb) 1168 | continue; 1169 | 1170 | /* we have hist_width bars, and we need to put the entire 1171 | * circle into that many bars. Devise which bar we're using 1172 | * for this particular lb->time */ 1173 | age = int( x_scale * (now - lb->time) ); 1174 | 1175 | /* add on x_scale; this is because if we are displaying 2 1176 | * seconds worth of data in one line, we only want to add on 1177 | * half. */ 1178 | bar_height[age] += x_scale; 1179 | } 1180 | 1181 | /* find the maximum bar height we have. */ 1182 | for(i = 0 ; i < hist_width ; ++i) 1183 | max_bar = getMAX(max_bar, bar_height[i]); 1184 | 1185 | y_scale = max_bar; 1186 | y_decr = ((float)y_scale / hist_height); 1187 | 1188 | for(i = 0 ; i < hist_height ; ++i) 1189 | { 1190 | if (i % 2 == 0) 1191 | mvprintw(HISTOGRAM_START + i, 0, "%3.0f", y_scale); 1192 | 1193 | mvaddch(HISTOGRAM_START + i, 3, '|'); 1194 | 1195 | /* compose a row of hashes */ 1196 | memset(line, ' ', hist_width); 1197 | line[hist_width] = '\0'; 1198 | for(j = 0 ; j < hist_width ; ++j) 1199 | { 1200 | if (bar_height[j] > y_scale) 1201 | line[j] = '#'; 1202 | } 1203 | mvprintw(HISTOGRAM_START + i, 4, "%s", line); 1204 | 1205 | y_scale -= y_decr; 1206 | } 1207 | 1208 | memset(horiz_line, '-', hist_width); 1209 | horiz_line[hist_width] = '\0'; 1210 | mvprintw(HISTOGRAM_START + hist_height, 2, "0+%*s", 1211 | hist_width, horiz_line); 1212 | 1213 | mvprintw(HISTOGRAM_START + hist_height+1, 4, "NOW"); 1214 | mvprintw(HISTOGRAM_START + hist_height+1, hist_width+3, "-%ds", 1215 | now - c->oldest()); 1216 | 1217 | refresh(); 1218 | } 1219 | -------------------------------------------------------------------------------- /src/display.h: -------------------------------------------------------------------------------- 1 | #ifndef _DISPLAY_H_ 2 | #define _DISPLAY_H_ 3 | 4 | /* macro to render "paused" in inverse at the coords given. This is 5 | * called from apachetop.cc/read_key() when pause is activated, and in each 6 | * display.cc/draw_header() when pause is turned on. 7 | */ 8 | #define DRAW_PAUSED(x,y) attron(A_REVERSE); \ 9 | mvaddstr(x, y, "paused"); \ 10 | attroff(A_REVERSE) 11 | 12 | #define SUBMENU_LINE_NUMBER LINES_RESERVED-2 13 | 14 | /* display() makes an array of these, sorts, and displays */ 15 | struct itemlist { 16 | unsigned int hash; 17 | int item, ip_item; 18 | double reqcount; 19 | double bytecount; 20 | time_t first, last, timespan; 21 | float rps, kbps; 22 | 23 | struct hitinfo r_codes[6]; 24 | }; 25 | 26 | bool display(time_t last_display); 27 | 28 | void display_header(); 29 | 30 | void display_list(); 31 | void display_sub_list(short display_mode_override, 32 | unsigned short offset, unsigned short limit); 33 | 34 | void translate_screen_to_pos(); 35 | 36 | void drawMarker(void); 37 | 38 | #define NO_INDENT 0 39 | void show_map_line(struct itemlist *item_ptr, int vert_location, map *m, 40 | unsigned short indent, unsigned short number_mode); 41 | 42 | void shellsort_wrapper(struct itemlist *items, unsigned int size, 43 | struct config pcf); 44 | void shellsort(struct itemlist *items, unsigned int size, int sorttype); 45 | 46 | float readableNum(double num, char *suffix); 47 | 48 | void display_submenu_banner(const char *title, int title_len, const char *banner); 49 | void clear_submenu_banner(void); 50 | 51 | void display_help(void); 52 | void display_active_filters(void); 53 | 54 | void display_histogram(); 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /src/filters.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | Filter::Filter(void) 4 | { 5 | #if HAVE_PCRE2_H 6 | regexp = NULL; 7 | #endif 8 | filter_text = NULL; 9 | } 10 | 11 | 12 | Filter::~Filter(void) 13 | { 14 | this->empty(); 15 | } 16 | 17 | 18 | void Filter::store(const char *filter) 19 | { 20 | if (filter_text) free(filter_text); 21 | filter_text = strdup(filter); 22 | 23 | #if HAVE_PCRE2_H 24 | if (regexp) delete regexp; 25 | try 26 | { 27 | regexp = new RegEx(filter); 28 | regex_isvalid = true; 29 | } 30 | catch (int err) 31 | { 32 | regexp = NULL; 33 | regex_isvalid = false; 34 | } 35 | #endif 36 | 37 | return; 38 | } 39 | 40 | 41 | bool Filter::isactive() 42 | { 43 | return filter_text ? true : false; 44 | } 45 | 46 | 47 | bool Filter::match(const char *string) 48 | { 49 | if (!filter_text) 50 | return false; 51 | 52 | #if HAVE_PCRE2_H 53 | if (regex_isvalid) 54 | return regexp->Search(string); 55 | #endif 56 | return strstr(string, filter_text); 57 | } 58 | 59 | 60 | void Filter::empty(void) 61 | { 62 | #if HAVE_PCRE2_H 63 | if (regexp) delete regexp; 64 | regexp = NULL; 65 | #endif 66 | if (filter_text) free(filter_text); 67 | filter_text = NULL; 68 | } 69 | -------------------------------------------------------------------------------- /src/filters.h: -------------------------------------------------------------------------------- 1 | #ifndef _FILTERS_H_ 2 | #define _FILTERS_H_ 3 | 4 | /* Filter class 5 | ** 6 | ** Each instance has one filter, which is either plaintext or regular 7 | ** expression (if HAVE_PCRE2_H is defined): Quick example: 8 | ** 9 | ** f = new Filter(); 10 | ** f->store("(movies|music)"); 11 | ** if (f->isactive() && f->match("some string with movies in it")) 12 | ** // you got a match 13 | */ 14 | 15 | class Filter 16 | { 17 | public: 18 | Filter(void); 19 | ~Filter(void); 20 | 21 | void store(const char *filter); 22 | bool isactive(void); 23 | bool match(const char *string); 24 | 25 | void empty(void); 26 | 27 | private: 28 | char *filter_text; 29 | 30 | #if HAVE_PCRE2_H 31 | bool regex_isvalid; 32 | RegEx *regexp; 33 | #endif 34 | 35 | }; 36 | 37 | #endif /* _FILTERS_H_ */ 38 | -------------------------------------------------------------------------------- /src/hits_circle.cc: -------------------------------------------------------------------------------- 1 | /* class to encapsulate set of functions to manage a circular array; 2 | * recent hit information is stored in the looping array; each hit 3 | * has information written into the next slot of structs. If 4 | * we reach the end, start over. Then the top-URL/IP is summed up from 5 | * this information. The bigger the table, the more hits you'll have 6 | * to summarise from. 7 | */ 8 | 9 | #include "apachetop.h" 10 | 11 | extern map *hm, *um, *rm; 12 | 13 | int Hits_Circle::create(unsigned int passed_size) 14 | { 15 | size = passed_size; 16 | pos = 0; 17 | walkpos = 0; 18 | 19 | tab = (circle_struct *) 20 | calloc(size, sizeof( circle_struct)); 21 | if (!tab) 22 | { 23 | abort(); 24 | } 25 | 26 | reqcount = bytecount = 0; 27 | memset(rc_summary, 0, sizeof(rc_summary)); 28 | 29 | return 0; 30 | } 31 | 32 | int Hits_Circle::insert(struct logbits lb) 33 | { 34 | circle_struct *posptr; 35 | short rc_tmp_old, rc_tmp_new; 36 | 37 | /* insert the given data into the current position, 38 | * and update pos to point at next position */ 39 | posptr = &tab[pos]; 40 | 41 | if (posptr->time == 0) 42 | /* if this is a new insert, increment our count */ 43 | ++reqcount; 44 | else 45 | { 46 | /* if this is re-using an old slot, remove refcount for the 47 | * previous data before we vape it */ 48 | hm->sub_ref(posptr->host_pos); 49 | um->sub_ref(posptr->url_pos); 50 | rm->sub_ref(posptr->ref_pos); 51 | 52 | } 53 | 54 | /* maintain some stats */ 55 | /* bytecount; remove the previous one and add the new one */ 56 | bytecount -= posptr->bytes; 57 | bytecount += lb.bytes; 58 | /* retcodes, remember how many we have of each */ 59 | rc_tmp_old = (int)posptr->retcode/100; 60 | rc_tmp_new = (int)lb.retcode/100; 61 | if (rc_tmp_old != rc_tmp_new) 62 | { 63 | --rc_summary[rc_tmp_old]; 64 | ++rc_summary[rc_tmp_new]; 65 | } 66 | 67 | /* store the data */ 68 | memcpy(posptr, &lb, sizeof(lb)); 69 | 70 | ++pos; 71 | 72 | /* see if we're running out of space. We'd like to keep however many 73 | * hits of data the user has asked for; if we're not managing 74 | * that, increase */ 75 | if (pos == size) 76 | { 77 | /* loop round */ 78 | pos = 0; 79 | } 80 | 81 | return 0; 82 | } 83 | 84 | //int Hits_Circle::walk(unsigned int *url_pos, unsigned int *ip_pos, 85 | // int *bytes, time_t *time, unsigned int *ipl, unsigned int *retcode) 86 | int Hits_Circle::walk(struct logbits **lb) 87 | { 88 | /* return each value in the circle one by one, starting at 0 and 89 | * working up; return 0 when there are more to go, or -1 when we're 90 | * done */ 91 | 92 | *lb = NULL; 93 | 94 | if (walkpos == size || tab[walkpos].time == 0) 95 | { 96 | walkpos = 0; 97 | return -1; 98 | } 99 | 100 | *lb = &tab[walkpos]; 101 | 102 | ++walkpos; 103 | return 0; 104 | } 105 | 106 | time_t Hits_Circle::oldest(void) 107 | { 108 | int tmp; 109 | 110 | /* return the first entry we have. normally this will be pos+1, but 111 | * cater for circumstances where it is isn't; ie we're initially 112 | * filling up the array (use 0), or we're at position size (use 0) */ 113 | if (pos == size) 114 | tmp = 0; /* earliest will be 0 */ 115 | else 116 | tmp = pos + 1; /* earliest is next element */ 117 | 118 | if (tab[tmp].time > 0) 119 | return tab[tmp].time; 120 | 121 | return tab[0].time; 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/hits_circle.h: -------------------------------------------------------------------------------- 1 | #ifndef _HITS_CIRCLE_H_ 2 | #define _HITS_CIRCLE_H_ 3 | 4 | class Hits_Circle : public Circle 5 | { 6 | public: 7 | int create(unsigned int passed_size); 8 | int insert(struct logbits lb); 9 | int walk(struct logbits **lb); 10 | 11 | void updatestats(void) {} 12 | time_t oldest(void); 13 | 14 | double getreqcount(void) { return reqcount; } 15 | double getbytecount(void) { return bytecount; } 16 | double getsummary(int r_c) { return rc_summary[r_c]; } 17 | 18 | private: 19 | int resize(int newsize); 20 | 21 | double reqcount, bytecount; 22 | double rc_summary[6]; 23 | 24 | typedef struct logbits circle_struct; 25 | circle_struct *tab; 26 | int size; /* total size of circle table */ 27 | int pos; /* where are we now? */ 28 | 29 | int walkpos; 30 | }; 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/inlines.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | #define THREE_QUARTERS 24 4 | #define ONE_EIGHTH 4 5 | #define HIGH_BITS (~((unsigned int)(~0) >> ONE_EIGHTH)) 6 | 7 | inline unsigned int StringHash(const char *str) 8 | { 9 | unsigned int val; 10 | unsigned int i; 11 | 12 | for (val = 0; *str; str++) 13 | { 14 | val = (val << ONE_EIGHTH) + *str; 15 | 16 | if ((i = val & HIGH_BITS) != 0) 17 | val = (val ^ (i >> THREE_QUARTERS)) & ~HIGH_BITS; 18 | } 19 | return val; 20 | } 21 | 22 | inline unsigned int QuickHash(const char *str) 23 | { 24 | unsigned int val, tmp; 25 | 26 | for(val = 0 ; *str ; str++) 27 | { 28 | val = (val << 4) + *str; 29 | if ((tmp = (val & 0xf0000000))) 30 | val = (val ^ (tmp >> 24)) ^ tmp; 31 | } 32 | return val; 33 | } 34 | 35 | inline unsigned long TTHash(const char *str) 36 | { 37 | unsigned long hash = 5381; 38 | int c; 39 | 40 | while ((c = *str++)) 41 | hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ 42 | 43 | return hash; 44 | } 45 | -------------------------------------------------------------------------------- /src/log.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | #include "inlines.cc" 4 | 5 | #define RESOLVING_STRING (char *)"..." 6 | #define NO_RESOLVED_INFO "?" 7 | 8 | extern map *um, /* urlmap */ 9 | *im, /* ipmap */ 10 | *hm, /* hostmap */ 11 | *rm, /* referrermap */ 12 | *fm; /* filemap */ 13 | 14 | extern time_t now; 15 | extern config cf; 16 | 17 | extern Circle *c; 18 | 19 | extern Queue want_host, want_ip; 20 | 21 | #if HAVE_ADNS_H 22 | extern adns_state adns; 23 | #endif 24 | 25 | /* CommonLogParser handles common and combined, despite its name */ 26 | int CommonLogParser::parse(char *logline, struct logbits *b) 27 | { 28 | char *bufsp, *bufcp, *ptr; 29 | char *workptr; 30 | 31 | struct sockaddr_in addr; 32 | 33 | bufsp = logline; 34 | 35 | /* host first */ 36 | bufcp = strchr(logline, ' '); 37 | if (!bufcp) 38 | return -1; 39 | 40 | *bufcp = '\0'; 41 | ++bufcp; 42 | 43 | /* quickly figure out if this is an IP or a host. We do this by 44 | * checking each character of it; if every character is either a 45 | * digit or a dot, then it's an IP (no host can just be digits) 46 | */ 47 | for(workptr = bufsp ; *workptr ; workptr++) 48 | { 49 | if (isdigit(*workptr)) continue; 50 | if (*workptr == '.') continue; 51 | 52 | /* it's neither a digit or a dot */ 53 | break; 54 | } 55 | 56 | ptr = bufsp; 57 | if (*workptr) 58 | { 59 | /* it is a hostname */ 60 | 61 | /* insert will return existing position if it exists */ 62 | b->host_pos = hm->insert(ptr); 63 | b->host_hash = TTHash(ptr); 64 | b->want_host = false; /* cos we have it */ 65 | 66 | #if HAVE_ADNS_H 67 | if (cf.do_resolving) 68 | { 69 | b->want_ip = true; 70 | 71 | dprintf("lookup %s\n", ptr); 72 | /* fire off a query with adns */ 73 | b->dns_query = new adns_query; 74 | adns_submit(adns, ptr, adns_r_a, 75 | (adns_queryflags) NULL, NULL, b->dns_query); 76 | 77 | b->ip_pos = im->insert(RESOLVING_STRING); 78 | b->ip_hash = TTHash(RESOLVING_STRING); 79 | } 80 | else 81 | #endif /* HAVE_ADNS_H */ 82 | { 83 | /* don't resolve the IP, and use -1 which means 84 | * "there is nothing of interest here" */ 85 | b->ip_pos = -1; 86 | b->want_ip = false; 87 | } 88 | } 89 | else 90 | { 91 | /* it is an IP */ 92 | 93 | b->ip_pos = im->insert(ptr); 94 | b->ip_hash = TTHash(ptr); 95 | b->want_ip = false; /* we have the IP already */ 96 | 97 | #if HAVE_ADNS_H 98 | if (cf.do_resolving) 99 | { 100 | 101 | /* this is so we'll get a display like 102 | ..resolving.. [212.13.201.101] 103 | then once resolved: 104 | clueful.shagged.org [212.13.201.101] 105 | */ 106 | b->host_pos = hm->insert(RESOLVING_STRING); 107 | b->host_hash = TTHash(RESOLVING_STRING); 108 | 109 | b->want_host = true; /* we're going to get this */ 110 | 111 | /* construct network byte order num 112 | ** for adns_submit_reverse 113 | */ 114 | addr.sin_family = AF_INET; 115 | addr.sin_addr.s_addr = inet_addr(ptr); 116 | 117 | b->dns_query = new adns_query; 118 | adns_submit_reverse(adns, (struct sockaddr *)&addr, 119 | adns_r_ptr, (adns_queryflags)adns_qf_owner, 120 | NULL, b->dns_query); 121 | } 122 | else 123 | #endif /* HAVE_ADNS_H */ 124 | { 125 | /* don't resolve the host, use the IP */ 126 | b->host_pos = hm->insert(ptr); 127 | b->host_hash = TTHash(ptr); 128 | b->want_host = false; /* we are not resolving */ 129 | } 130 | } 131 | 132 | /* now skip to date */ 133 | if (!(bufcp = strchr(bufcp, '['))) 134 | return -1; 135 | 136 | bufcp++; 137 | 138 | b->time = now; /* be lazy */ 139 | 140 | /* find the end of the date */ 141 | if (!(bufcp = strchr(bufcp, ']'))) 142 | return -1; 143 | 144 | bufcp += 3; /* from end of date to first char of method */ 145 | 146 | /* URL. processURL() will update bufcp to point at the end so we can 147 | * continue processing from there */ 148 | if ((ptr = this->processURL(&bufcp)) == NULL) 149 | return -1; 150 | 151 | /* get url_pos for this url; for circle_struct (c) later */ 152 | b->url_pos = um->insert(ptr); 153 | b->url_hash = TTHash(ptr); 154 | 155 | /* return code */ 156 | b->retcode = atoi(bufcp); 157 | bufcp += 4; 158 | 159 | /* bytecount */ 160 | b->bytes = atoi(bufcp); 161 | 162 | 163 | /* this may be the end of the line if it's a common log; if 164 | * it's combined then we have referrer and user agent left */ 165 | if (!(bufsp = strchr(bufcp, '"'))) 166 | { 167 | /* nothing left, its common */ 168 | 169 | /* fill in a dummy value for referrer map */ 170 | b->ref_pos = rm->insert((char *)"Unknown"); 171 | return 0; 172 | } 173 | 174 | bufsp += 1; /* skip to first character of referrer */ 175 | 176 | /* find the end of referrer and null it */ 177 | if (!(bufcp = strchr(bufsp, '"'))) 178 | return -1; 179 | *bufcp = '\0'; 180 | 181 | /* unless they want to keep it, skip over the protocol, ie http:// */ 182 | if ((cf.preserve_ref_protocol == 0) && (bufcp = strstr(bufsp, "://"))) 183 | bufsp = bufcp + 3; 184 | 185 | 186 | /* we could munge the referrer now; cut down the path elements, 187 | * remove querystring, but we'll leave that for a later date */ 188 | 189 | // b->referrer = bufsp; 190 | 191 | /* get ref_pos for this url; for circle_struct (c) later */ 192 | b->ref_pos = rm->insert(bufsp); 193 | b->ref_hash = TTHash(bufsp); 194 | 195 | /* user-agent is as yet unused */ 196 | 197 | return 0; 198 | } 199 | 200 | 201 | int AtopLogParser::parse(char *logline, struct logbits *b) 202 | { 203 | return 0; 204 | } 205 | 206 | 207 | /* generic parser helper functions */ 208 | 209 | char *LogParser::processURL(char **buf) /* {{{ */ 210 | { 211 | char *bufcp, *realstart, *endptr; 212 | int length; 213 | 214 | bufcp = *buf; 215 | 216 | /* this skips past the method */ 217 | if (!(bufcp = strchr(bufcp, ' ')) ) 218 | return NULL; 219 | ++bufcp; // skip space 220 | 221 | realstart = bufcp; 222 | 223 | /* find the end of url; locate a protocol, out of the following list */ 224 | if ( 225 | !(endptr = strstr(bufcp, " HTTP/")) 226 | #if WITH_REAL_PROTOCOLS 227 | /* v0.12: RealServer logs are very similar to Apache's, 228 | * so we can support those too! Cool! */ 229 | && !(endptr = strstr(bufcp, " RTSP/")) /* RealStreaming UDP */ 230 | && !(endptr = strstr(bufcp, " RTSPT/")) /* RealStreaming TCP */ 231 | && !(endptr = strstr(bufcp, " RTSPH/")) /* RealStreaming HTTP */ 232 | #endif 233 | ) 234 | return NULL; 235 | 236 | /* null the space in front of it */ 237 | *endptr = '\0'; 238 | 239 | /* TODO maybe we can use the protocol someday.. */ 240 | 241 | 242 | /* this is all mungeURL is interested in */ 243 | length = endptr - realstart; 244 | 245 | /* now find the finishing ", so parse* can deal with rest of line */ 246 | if (!(endptr = strstr(endptr+1, "\" "))) 247 | return NULL; 248 | 249 | mungeURL(&realstart, &length); 250 | 251 | /* feed back where the end of the URL is */ 252 | *buf = endptr+2; 253 | 254 | return realstart; 255 | } /* }}} */ 256 | 257 | /* munge the url passed in *url inplace; 258 | * *length is the original length, and we update it once we're done */ 259 | int LogParser::mungeURL(char **url, int *length) /* {{{ */ 260 | { 261 | int skipped = 0; 262 | char *bufcp, *endptr, *workptr; 263 | 264 | endptr = *url + *length; 265 | *endptr = '\0'; 266 | 267 | /* do we want to keep the query string? */ 268 | if (!cf.keep_querystring) 269 | { 270 | /* null the first ? or & - anything after 271 | * it is unrequired; it's the querystring */ 272 | if ((workptr = strchr(*url, '?')) || 273 | (workptr = strchr(*url, '&')) ) 274 | { 275 | /* we might have overrun the end of the real URL and 276 | * gone into referrer or something. Check that. */ 277 | if (workptr < endptr) 278 | { 279 | /* we're ok */ 280 | *workptr = '\0'; 281 | bufcp = workptr+1; 282 | } 283 | } 284 | } 285 | 286 | /* how many path segments of the url are we keeping? */ 287 | if (cf.keep_segments > 0) 288 | { 289 | /* given a path of /foo/bar/moo/ and a keep_segments of 2, 290 | * we want the / after the second element */ 291 | 292 | bufcp = workptr = *url + 1; /* skip leading / */ 293 | 294 | //dprintf("workptr is %s\n", workptr); 295 | 296 | /* now skip the next keep_segments slashes */ 297 | while (skipped < cf.keep_segments && workptr < endptr) 298 | { 299 | workptr++; 300 | 301 | if (*workptr == '/') 302 | { 303 | /* discovered a slash */ 304 | skipped++; 305 | 306 | /* bufcp becomes the char after / */ 307 | bufcp = workptr+1; 308 | } 309 | 310 | /* if we hit the end before finding the right number 311 | * of slashes, we just keep it all */ 312 | if (workptr == endptr) 313 | bufcp = workptr; 314 | } 315 | *bufcp = '\0'; 316 | } 317 | 318 | 319 | /* do we want to lowercase it all? */ 320 | if (cf.lowercase_urls) 321 | { 322 | workptr = *url; 323 | while(workptr < endptr) 324 | { 325 | *workptr = tolower(*workptr); 326 | workptr++; 327 | } 328 | } 329 | 330 | /* fin */ 331 | 332 | return 0; 333 | } /* }}} */ 334 | 335 | #if HAVE_ADNS_H 336 | /* adns; check to see if any queries have returned, and populate the circle 337 | * as required. Be careful of any circle entries that have expired since 338 | * the query was started. */ 339 | void collect_dns_responses() 340 | { 341 | int err; 342 | struct logbits *lb; 343 | adns_answer *answer; 344 | int got_host = false, got_ip = false; 345 | 346 | /* check every circle entry that has want_host or want_ip */ 347 | 348 | while(c->walk(&lb) != -1) 349 | { 350 | if (lb->want_host == false && lb->want_ip == false) 351 | continue; 352 | 353 | // dprintf("adns_check for %p\n", lb); 354 | /* this circle slot has an outstanding query */ 355 | err = adns_check(adns, lb->dns_query, &answer, NULL); 356 | 357 | if (err == EAGAIN) 358 | { 359 | /* still waiting */ 360 | continue; 361 | } 362 | 363 | /* some form of reply. Be it success or error, this query is 364 | * now done. */ 365 | 366 | got_host = lb->want_host; 367 | got_ip = lb->want_ip; 368 | 369 | lb->want_host = false; 370 | lb->want_ip = false; 371 | delete lb->dns_query; 372 | 373 | if (answer->status == adns_s_ok) 374 | { 375 | /* we have a reply */ 376 | // dprintf("got a reply\n"); 377 | if (got_host) 378 | { 379 | /* we'll have this new host in the hostmap ta */ 380 | lb->host_pos = hm->insert(*answer->rrs.str); 381 | lb->host_hash = TTHash(*answer->rrs.str); 382 | } 383 | else if (got_ip) 384 | { 385 | /* put the IP into the ipmap */ 386 | lb->ip_pos = 387 | im->insert(inet_ntoa(*answer->rrs.inaddr)); 388 | lb->ip_hash = 389 | TTHash(inet_ntoa(*answer->rrs.inaddr)); 390 | } 391 | 392 | free(answer); 393 | continue; 394 | } 395 | 396 | /* assume this IP has no reverse info; so we'll put the IP 397 | * into Host as well; this is so that the Host list will be 398 | * maintained properly (if we just put ? into Host, then 399 | * they bunch up together) 400 | */ 401 | 402 | lb->host_pos = hm->insert(im->reverse(lb->ip_pos)); 403 | lb->host_hash = TTHash(im->reverse(lb->ip_pos)); 404 | free(answer); 405 | continue; 406 | } 407 | } 408 | #endif /* HAVE_ADNS_H */ 409 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #ifndef _LOG_H_ 2 | #define _LOG_H_ 3 | 4 | /* types */ 5 | #define LOG_COMMON 1 6 | #define LOG_COMBINED 2 7 | #define LOG_ATOP 3 8 | 9 | /* parse*() fills in this struct */ 10 | struct logbits { 11 | int url_pos; /* location of url in urlmap */ 12 | int url_hash; /* hash of url string */ 13 | 14 | /* host and ip. Whatever we have in the log gets set here initially, 15 | ** then want_foo is set to false. want_otherfoo is set to true 16 | ** explicitly; then an entry is made into the relevant Queue 17 | ** (want_host or want_ip). adns does the resolving, and then 18 | ** whatever it finds is placed in otherfoo, and want_otherfoo is set 19 | ** to false */ 20 | bool want_host; 21 | int host_pos; /* location of host in hostmap */ 22 | int host_hash; /* hash of host string */ 23 | 24 | bool want_ip; 25 | //unsigned int ipl; /* numerical representation of IP */ 26 | int ip_pos; /* location of IP in ipmap */ 27 | int ip_hash; /* hash of IP string */ 28 | 29 | #if HAVE_ADNS_H 30 | adns_query *dns_query; 31 | //Resolver dns_query; 32 | #endif 33 | 34 | int ref_pos; /* location of referrer in map */ 35 | int ref_hash; /* hash of Referrer string */ 36 | 37 | int fileid; /* which file descriptor we're from */ 38 | int file_pos; /* location of file in filemap */ 39 | 40 | unsigned short retcode; /* return code */ 41 | unsigned int bytes; /* body of result page */ 42 | time_t time; /* time of request, unixtime */ 43 | }; 44 | 45 | class LogParser 46 | { 47 | public: 48 | virtual int parse(char *logline, struct logbits *b) = 0; 49 | 50 | char *processURL(char **buf); 51 | int mungeURL(char **url, int *length); 52 | }; 53 | 54 | class CommonLogParser : public LogParser 55 | { 56 | int parse(char *logline, struct logbits *b); 57 | }; 58 | class AtopLogParser : public LogParser 59 | { 60 | int parse(char *logline, struct logbits *b); 61 | }; 62 | 63 | void collect_dns_responses(); 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /src/map.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | /* string to hash map, and vice versa; for quick string lookups */ 4 | 5 | extern Circle *c; 6 | extern time_t now; 7 | 8 | int map::create(int passed_size) 9 | { 10 | size = passed_size; 11 | 12 | tab = (struct hash_struct *)malloc(size * sizeof(struct hash_struct)); 13 | 14 | /* initialise map to be empty */ 15 | this->empty(0, size); 16 | 17 | /* a hash for quick string lookups */ 18 | tab_hash = new OAHash; 19 | tab_hash->create(size*9); 20 | 21 | return 0; 22 | } 23 | 24 | void map::empty(int from, int to) 25 | { 26 | int x; 27 | struct hash_struct *ptr; 28 | 29 | for(ptr = &tab[from], x = from; x < to; ++ptr, ++x) 30 | { 31 | ptr->refcount = 0; 32 | ptr->pos = x; 33 | ptr->string = NULL; 34 | ptr->time = 0; 35 | } 36 | 37 | return; 38 | } 39 | 40 | int map::destroy(void) 41 | { 42 | free(tab); 43 | 44 | tab_hash->destroy(); 45 | delete tab_hash; 46 | 47 | return 0; 48 | } 49 | 50 | int map::resize(int newsize) 51 | { 52 | int x; 53 | struct hash_struct *newtab; 54 | 55 | /* we have to resize the tab, then clear the hash, remake the hash 56 | * at the newsize*5, then re-populate the hash. Remaking the hash is 57 | * quite expensive, so we'd like to do this as infrequently as 58 | * possible */ 59 | newtab = (struct hash_struct *) 60 | realloc(tab, newsize * sizeof(struct hash_struct)); 61 | 62 | /* did it work? */ 63 | if (!newtab) 64 | { 65 | perror("realloc map"); 66 | exit(1); 67 | } 68 | 69 | tab = newtab; 70 | 71 | /* empty the new area */ 72 | this->empty(size, newsize); 73 | 74 | /* remake the hash since memory addresses may have moved */ 75 | delete tab_hash; 76 | tab_hash = new OAHash; 77 | tab_hash->create(newsize*9); 78 | for (x = 0 ; x < size ; ++x) 79 | { 80 | tab_hash->insert(tab[x].string, &tab[x]); 81 | } 82 | 83 | // dprintf("%p resize from %d to %d\n", this, size, newsize); 84 | 85 | size = newsize; 86 | 87 | return 0; 88 | } 89 | 90 | int map::insert(char *string) 91 | { 92 | int x; 93 | struct hash_struct *ptr; 94 | 95 | /* if this string is in the map, return existing position only */ 96 | if ((x = this->lookup(string)) != -1) 97 | { 98 | /* we wanted to insert, but didn't, so the refcount for this 99 | * particular entry is incremented */ 100 | tab[x].refcount++; 101 | 102 | // dprintf("%d Found %p %d for %s\n", time(NULL), this, x, string); 103 | return x; 104 | } 105 | 106 | /* find free table slot FIXME make this more efficient */ 107 | for (ptr = &tab[0], x = 0; x <= size; ++ptr, ++x) 108 | { 109 | if (x == size) 110 | { 111 | /* none free, make room */ 112 | this->resize(size * 2); 113 | 114 | /* realloc may well move the table in 115 | * memory so update the pointer */ 116 | ptr = &tab[x]; 117 | 118 | break; 119 | } 120 | 121 | /* if this map entry has a refcount of zero (this is new in 122 | * v0.12) then nothing is pointing at it, hopefully, so it 123 | * can be nuked */ 124 | if (ptr->refcount == 0) 125 | break; 126 | } 127 | 128 | /* if we are re-using, free up the old data */ 129 | if (ptr->string) 130 | { 131 | tab_hash->remove(ptr->string); 132 | free(ptr->string); 133 | } 134 | 135 | // dprintf("%d Using %p %d for %s\n", time(NULL), this, x, string); 136 | 137 | /* make entry in our table */ 138 | ptr->time = now; 139 | ptr->refcount = 1; 140 | ptr->string = strdup(string); 141 | 142 | /* add into hash */ 143 | tab_hash->insert(ptr->string, ptr); 144 | 145 | return x; 146 | } 147 | 148 | int map::remove(char *string) 149 | { 150 | struct hash_struct *ptr; 151 | 152 | /* find string in hash */ 153 | if ((ptr = (struct hash_struct *)tab_hash->lookup(string))) 154 | { 155 | // dprintf("%d Remove %p %d for %s\n", time(NULL), this, ptr->pos, string); 156 | 157 | /* remove from table */ 158 | free(ptr->string); 159 | ptr->string = NULL; 160 | ptr->time = 0; 161 | ptr->refcount = 0; 162 | 163 | /* remove from hash */ 164 | tab_hash->remove(string); 165 | } 166 | 167 | return 0; 168 | } 169 | 170 | int map::lookup(char *string) 171 | { 172 | struct hash_struct *ptr; 173 | 174 | if ((ptr = (struct hash_struct *)tab_hash->lookup(string))) 175 | { 176 | ptr->time = now; /* touch it, so insert won't remove */ 177 | return ptr->pos; 178 | } 179 | 180 | return -1; 181 | } 182 | 183 | char *map::reverse(int pos) 184 | { 185 | /* return pointer to char for this string pos */ 186 | return tab[pos].string; 187 | } 188 | 189 | void map::sub_ref(int pos) 190 | { 191 | // dprintf("%d subref %p %d for %s\n", 192 | // time(NULL), this, pos, tab[pos].string); 193 | 194 | if (tab[pos].refcount > 0) 195 | tab[pos].refcount--; 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/map.h: -------------------------------------------------------------------------------- 1 | #ifndef _MAP_H_ 2 | #define _MAP_H_ 3 | 4 | class map 5 | { 6 | public: 7 | int create(int passed_size); 8 | void empty(int from, int to); 9 | int destroy(void); 10 | int resize(int newsize); 11 | int insert(char *string); 12 | int remove(char *string); 13 | int lookup(char *string); 14 | 15 | 16 | char *reverse(int pos); 17 | void sub_ref(int pos); 18 | 19 | private: 20 | struct hash_struct 21 | { 22 | unsigned int refcount; 23 | 24 | int pos; 25 | char *string; 26 | 27 | time_t time; 28 | }; 29 | 30 | struct hash_struct *tab; 31 | int size; 32 | 33 | OAHash *tab_hash; 34 | }; 35 | #endif 36 | -------------------------------------------------------------------------------- /src/ohtbl.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | #include "inlines.cc" 4 | 5 | #define OACMP(a, b) (strcmp((char *)a, (char *)b) == 0) ? 1 : 0 6 | #define OA_H1(k) (TTHash(k) % positions) 7 | #define OA_H2(k) (1 + (StringHash(k) % (positions-2))) 8 | #define OA_HASH(k) (OA_H1(k) + (i * OA_H2(k))) % positions 9 | 10 | static int primes[] = {101, 241, 499, 1009, 2003, 3001, 4001, 5003, 11 | 10007, 15013, 19381, 29131, 38011, 45589, 12 | 80021, 100003, 150001, 200003, 500009, 5000011, 0}; 13 | 14 | int OAHash::getNextPrime(int size) 15 | { 16 | int *prime; 17 | for (prime = &primes[0] ; *prime ; prime++) 18 | if (*prime > size) 19 | return *prime; 20 | 21 | return size*2; /* we're out of primes so give up */ 22 | } 23 | 24 | int OAHash::create(int p_positions) 25 | { 26 | unsigned int i; 27 | 28 | /* use the next prime up from p_positions */ 29 | if ((p_positions = getNextPrime(p_positions)) == -1) 30 | abort(); 31 | 32 | if ((table = (ohEntry *)malloc(p_positions * sizeof(ohEntry))) == NULL) 33 | return -1; 34 | 35 | positions = p_positions; 36 | 37 | for(i = 0 ; i < positions ; ++i) 38 | table[i].key = NULL; 39 | 40 | size = 0; 41 | 42 | return 0; 43 | } 44 | 45 | void OAHash::destroy(void) 46 | { 47 | free(table); 48 | 49 | return; 50 | } 51 | 52 | void *OAHash::insert(char *key, void *data) 53 | { 54 | unsigned int p, i; 55 | void *d; 56 | 57 | // Do not exceed the number of positions in the table. 58 | if (size == positions) 59 | return NULL; 60 | 61 | // Do nothing if the data is already in the table. 62 | if ((d = lookup(key))) 63 | /* return the position in case it can be used */ 64 | return d; 65 | 66 | for (i = 0; i < positions; ++i) 67 | { 68 | p = OA_HASH(key); 69 | if (table[p].key == NULL || table[p].key == &vacated) 70 | { 71 | // Insert the data into the table. 72 | table[p].key = key; 73 | table[p].data = data; 74 | size++; 75 | return data; 76 | } 77 | } 78 | 79 | return NULL; 80 | } 81 | 82 | int OAHash::remove(char *key) 83 | { 84 | unsigned int p, i; 85 | 86 | for (i = 0; i < positions; ++i) 87 | { 88 | p = OA_HASH(key); 89 | if (table[p].key == NULL) 90 | { 91 | // Return that the data was not found. 92 | return -1; 93 | } 94 | else if (table[p].key == &vacated) 95 | { 96 | // Search beyond vacated positions. 97 | continue; 98 | } 99 | else if (OACMP(table[p].key, key)) 100 | { 101 | table[p].key = &vacated; 102 | size--; 103 | return 0; 104 | } 105 | } 106 | return -1; 107 | } 108 | 109 | void *OAHash::lookup(char *key) 110 | { 111 | unsigned int p, i; 112 | 113 | for (i = 0; i < positions; ++i) 114 | { 115 | p = OA_HASH(key); 116 | if (table[p].key == NULL) 117 | { 118 | return NULL; 119 | } 120 | 121 | else if (table[p].key == &vacated) 122 | { 123 | continue; 124 | } 125 | 126 | else if (OACMP(table[p].key, key)) 127 | // return pointer to data 128 | return table[p].data; 129 | 130 | } 131 | return NULL; 132 | } 133 | -------------------------------------------------------------------------------- /src/ohtbl.h: -------------------------------------------------------------------------------- 1 | #ifndef _OHTBL_H_ 2 | #define _OHTBL_H_ 3 | 4 | class OAHash 5 | { 6 | public: 7 | int create(int p_positions); 8 | 9 | void destroy(void); 10 | void *insert(char *key, void *data); 11 | int remove(char *key); 12 | void *lookup(char *key); 13 | 14 | unsigned int size; 15 | 16 | private: 17 | int getNextPrime(int size); 18 | int cmp(const void *a, const void *b); 19 | 20 | typedef struct 21 | { 22 | char *key; 23 | void *data; 24 | } ohEntry; 25 | 26 | unsigned int positions; 27 | ohEntry *table; 28 | char vacated; 29 | }; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/pcre2_cpp_wrapper.h: -------------------------------------------------------------------------------- 1 | // 2 | // Adapted for PCRE2 (c) 2023 Helmut K. C. Tessarek (tessarek@evermeet.cx) 3 | // removed unnecessary methods (not needed by apachetop) 4 | // 5 | // regex.hpp 1.0 Copyright (c) 2003 Peter Petersen (pp@on-time.de) 6 | // Simple C++ wrapper for PCRE 7 | // 8 | // This source file is freeware. You may use it for any purpose without 9 | // restriction except that the copyright notice as the top of this file as 10 | // well as this paragraph may not be removed or altered. 11 | // 12 | // This header file declares class RegEx, a simple and small API wrapper 13 | // for PCRE. 14 | // 15 | // RegEx::RegEx(const char * regex, int options = 0) 16 | // 17 | // The constructor's first parameter is the regular expression the 18 | // created object shall implement. Optional parameter options can be 19 | // any combination of PCRE options accepted by pcre_compile(). If 20 | // compiling the regular expression fails, an error message string is 21 | // thrown as an exception. 22 | // 23 | // RegEx::~RegEx() 24 | // 25 | // The destructor frees all resources held by the RegEx object. 26 | // 27 | // int RegEx::SubStrings(void) const 28 | // 29 | // Method SubStrings() returns the number of substrings defined by 30 | // the regular expression. The match of the entire expression is also 31 | // considered a substring, so the return value will always be >= 1. 32 | // 33 | // bool RegEx::Search(const char * subject, int len = -1, int options = 0) 34 | // 35 | // Method Search() applies the regular expression to parameter subject. 36 | // Optional parameter len can be used to pass the subject's length to 37 | // Search(). If not specified (or less than 0), strlen() is used 38 | // internally to determine the length. Parameter options can contain 39 | // any combination of options PCRE_ANCHORED, PCRE_NOTBOL, PCRE_NOTEOL. 40 | // PCRE_NOTEMPTY. Search() returns true if a match is found. 41 | // 42 | // bool RegEx::SearchAgain(int options = 0) 43 | // 44 | // SearchAgain() again applies the regular expression to parameter 45 | // subject last passed to a successful call of Search(). It returns 46 | // true if a further match is found. Subsequent calls to SearchAgain() 47 | // will find all matches in subject. Example: 48 | // 49 | // if (Pattern.Search(astring)) { 50 | // do { 51 | // printf("%s\n", Pattern.Match()); 52 | // } while (Pattern.SearchAgain()); 53 | // } 54 | // 55 | // Parameter options is interpreted as for method Search(). 56 | // 57 | // const char * RegEx::Match(int i = 1) 58 | // 59 | // Method Match() returns a pointer to the matched substring specified 60 | // with parameter i. Match() may only be called after a successful 61 | // call to Search() or SearchAgain() and applies to that last 62 | // Search()/SearchAgain() call. Parameter i must be less than 63 | // SubStrings(). Match(-1) returns the last searched subject. 64 | // Match(0) returns the match of the complete regular expression. 65 | // Match(1) returns $1, etc. 66 | // 67 | // The bottom of this file contains an example using class RegEx. It's 68 | // the simplest version of grep I could come with. You can compile it by 69 | // defining REGEX_DEMO on the compiler command line. 70 | // 71 | 72 | #ifndef _REGEX_H 73 | #define _REGEX_H 74 | 75 | #include 76 | 77 | #ifndef _PCRE2_H 78 | #define PCRE2_CODE_UNIT_WIDTH 8 79 | #include "pcre2.h" 80 | #endif 81 | 82 | class RegEx 83 | { 84 | public: 85 | ///////////////////////////////// 86 | RegEx(const char *regex, int options = 0) 87 | { 88 | int error; 89 | PCRE2_SIZE erroroffset; 90 | PCRE2_SPTR pattern; 91 | PCRE2_SIZE pattern_length; 92 | 93 | pattern = (PCRE2_SPTR)regex; 94 | pattern_length = (PCRE2_SIZE)strlen((char *)regex); 95 | 96 | re = pcre2_compile(pattern, pattern_length, options, &error, &erroroffset, NULL); 97 | if (re == NULL) 98 | { 99 | #if PCRE2_WRAPPER_DEBUG 100 | PCRE2_UCHAR buffer[256]; 101 | pcre2_get_error_message(error, buffer, sizeof(buffer)); 102 | fprintf(stderr, "PCRE2 compilation failed at offset %d: %s\n", (int)erroroffset, buffer); 103 | #endif 104 | throw error; 105 | } 106 | match_data = pcre2_match_data_create_from_pattern(re, NULL); 107 | }; 108 | 109 | ///////////////////////////////// 110 | ~RegEx() 111 | { 112 | ClearMatchData(); 113 | pcre2_code_free(re); 114 | } 115 | 116 | ///////////////////////////////// 117 | bool Search(const char *text) 118 | { 119 | int rc = 0; 120 | PCRE2_SPTR subject; 121 | PCRE2_SIZE subject_length; 122 | 123 | subject = (PCRE2_SPTR)text; 124 | subject_length = (PCRE2_SIZE)strlen((char *)text); 125 | 126 | rc = pcre2_match(re, subject, subject_length, 0, 0, match_data, NULL); 127 | #if PCRE2_WRAPPER_DEBUG 128 | fprintf(stderr, "PCRE2 pcre2_match rc: %d\n", rc); 129 | #endif 130 | return rc > 0; 131 | } 132 | 133 | private: 134 | inline void ClearMatchData(void) 135 | { 136 | if (match_data) 137 | { 138 | pcre2_match_data_free(match_data); 139 | match_data = NULL; 140 | } 141 | } 142 | pcre2_code *re; 143 | pcre2_match_data *match_data; 144 | }; 145 | 146 | // Below is a little demo/test program using class RegEx 147 | 148 | #ifdef REGEX_DEMO 149 | 150 | #include 151 | #include "regex.hpp" 152 | 153 | /////////////////////////////////////// 154 | int main(int argc, char * argv[]) 155 | { 156 | if (argc != 2) 157 | { 158 | fprintf(stderr, "Usage: grep pattern\n\n" 159 | "Reads stdin, searches 'pattern', writes to stdout\n"); 160 | return 2; 161 | } 162 | try 163 | { 164 | RegEx Pattern(argv[1]); 165 | int count = 0; 166 | char buffer[1024]; 167 | 168 | while (fgets(buffer, sizeof(buffer), stdin)) 169 | if (Pattern.Search(buffer)) 170 | fputs(buffer, stdout), 171 | count++; 172 | return count == 0; 173 | } 174 | catch (const char * ErrorMsg) 175 | { 176 | fprintf(stderr, "error in regex '%s': %s\n", argv[1], ErrorMsg); 177 | return 2; 178 | } 179 | } 180 | 181 | #endif // REGEX_DEMO 182 | 183 | #endif // _REGEX_H 184 | -------------------------------------------------------------------------------- /src/queue.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | Queue::Queue(void) 4 | { 5 | size = 0; 6 | head = NULL; 7 | tail = NULL; 8 | } 9 | Queue::~Queue(void) 10 | { 11 | delete head; 12 | } 13 | 14 | void Queue::push(void *q) 15 | { 16 | Node *temp; 17 | 18 | temp = new Node; 19 | 20 | temp->x = q; 21 | temp->next = NULL; 22 | 23 | if (size == 0) 24 | { 25 | head = temp; 26 | tail = temp; 27 | } 28 | else 29 | { 30 | tail->next = temp; 31 | tail = temp; 32 | } 33 | size++; 34 | } 35 | 36 | // remove and return first entry 37 | int Queue::pop(void **q) 38 | { 39 | Node *temp; 40 | 41 | if (!head) 42 | { 43 | *q = NULL; 44 | return -1; 45 | } 46 | 47 | // return the first entry 48 | *q = head->x; 49 | 50 | if (head->next) 51 | // we have another node to point at 52 | temp = head->next; 53 | else 54 | // no more nodes 55 | temp = NULL; 56 | 57 | delete head; 58 | head = temp; 59 | 60 | size--; 61 | 62 | return 0; // success 63 | } 64 | 65 | int Queue::entries(void) 66 | { 67 | return size; 68 | } 69 | -------------------------------------------------------------------------------- /src/queue.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUEUE_H_ 2 | #define _QUEUE_H_ 3 | 4 | class Node 5 | { 6 | public: 7 | void *x; 8 | Node *next; 9 | 10 | Node() { next = NULL; } 11 | }; 12 | 13 | 14 | class Queue 15 | { 16 | private: 17 | Node *head; 18 | Node *tail; 19 | int size; 20 | 21 | public: 22 | Queue(void); 23 | ~Queue(void); 24 | 25 | void push(void *r); 26 | int pop(void **r); 27 | int entries(void); 28 | }; 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/resolver.cc: -------------------------------------------------------------------------------- 1 | /* 2 | ApacheTop resolver functions 3 | 4 | parse() will call add_request() 5 | this checks to see if we have an IP -> host mapping already 6 | if not it fires off adns_submit_reverse 7 | 8 | 9 | */ 10 | 11 | #if HAVE_ADNS_H 12 | 13 | #include "apachetop.h" 14 | 15 | Resolver::Resolver(void) 16 | { 17 | if (adns_init(&adns, adns_if_noenv, 0) != 0) 18 | { 19 | perror("adns_init"); 20 | exit(1); 21 | } 22 | } 23 | 24 | Resolver::~Resolver(void) 25 | { 26 | adns_finish(adns); 27 | } 28 | 29 | int Resolver::add_request(char *request, enum resolver_action act) 30 | { 31 | struct sockaddr_in addr; 32 | 33 | /* depending on resolver_action, this is an IP or a host to lookup 34 | * the other way */ 35 | 36 | /* at the moment we only do IPs */ 37 | 38 | switch(act) 39 | { 40 | case resolver_gethost: 41 | 42 | /* see if this IP is in host_ip_tablei; if it is we may be 43 | * able to get a host for it (or an adns is already looking 44 | * it up/has looked it up and got negative reply) */ 45 | 46 | 47 | addr.sin_family = AF_INET; 48 | /* add error checking */ 49 | #if HAVE_INET_ATON 50 | inet_aton(request, &(addr.sin_addr)); 51 | #else 52 | addr.sin_addr.s_addr = inet_addr(request); 53 | #endif 54 | 55 | 56 | // adns_submit_reverse(adns, (struct sockaddr *)&addr, adns_r_ptr, 57 | // (enum adns_queryflags)adns_qf_owner, NULL, b->dns_query); 58 | 59 | break; 60 | } 61 | } 62 | #endif /* HAVE_ADNS_H */ 63 | -------------------------------------------------------------------------------- /src/resolver.h: -------------------------------------------------------------------------------- 1 | #ifndef _RESOLVER_H_ 2 | #define _RESOLVER_H_ 3 | 4 | enum resolver_action 5 | { 6 | resolver_getip = 0, 7 | resolver_gethost = 1 8 | }; 9 | 10 | class Resolver 11 | { 12 | public: 13 | Resolver(void); 14 | ~Resolver(void); 15 | int add_request(char *request, enum resolver_action act); 16 | 17 | private: 18 | adns_state adns; 19 | 20 | struct host_ip_table 21 | { 22 | unsigned long ipl; 23 | 24 | char *host; 25 | }; 26 | 27 | OAHash *host_ip_hash; 28 | }; 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/timed_circle.cc: -------------------------------------------------------------------------------- 1 | #include "apachetop.h" 2 | 3 | /* Timed_Circle class; stores all the hits in the last $X seconds. 4 | * 5 | * tab is a timed_circle_struct pointer, which we malloc to be $X; thus each 6 | * struct is one second. 7 | * 8 | * tab[x].hits is a hit_struct pointer, malloc'ed to roughly how many 9 | * hits we expect to see per second. This can be intelligently guessed from 10 | * past data where possible. Each struct within this is then one hit. 11 | */ 12 | 13 | #define MAX_BUCKET (bucketsize) 14 | 15 | extern time_t now; /* global ApacheTop-wide to save on time() calls */ 16 | extern struct gstat gstats; 17 | 18 | extern map *hm, *um, *rm; 19 | 20 | int Timed_Circle::create(unsigned int size) 21 | { 22 | /* increment size by 1, because one bucket is always being used for 23 | * a transitional second; thus could be empty. So we always have the 24 | * requested amount of data (or perhaps slightly more) */ 25 | size++; 26 | 27 | /* malloc the buckets */ 28 | tab = (struct timed_circle_struct *) 29 | malloc(size * sizeof(struct timed_circle_struct)); 30 | 31 | if (tab == NULL) 32 | return -1; 33 | 34 | this->initbuckets(0, size); 35 | 36 | bucketsize = size; 37 | bucketpos = 0; /* start at the beginning */ 38 | 39 | walk_hitpos = walk_bucketpos = 0; 40 | 41 | return 0; 42 | } 43 | 44 | int Timed_Circle::initbuckets(const unsigned int from, const unsigned int to) 45 | { 46 | unsigned int x; 47 | struct timed_circle_struct *ptr; 48 | 49 | /* clear some values to 0 */ 50 | for(x = from, ptr = &tab[x] ; x < to; ++x, ++ptr) 51 | { 52 | /* set up hits array for this bucket */ 53 | 54 | ptr->hitsize = 5; 55 | ptr->hits = (HIT *)malloc(sizeof(HIT) * ptr->hitsize); 56 | if (!ptr->hits) abort(); 57 | 58 | memset(ptr->hits, 0, sizeof(HIT) * ptr->hitsize); 59 | 60 | this->resetbucketstats(x); 61 | } 62 | 63 | return 0; 64 | } 65 | 66 | void Timed_Circle::resetbucketstats(const unsigned int r) 67 | { 68 | int x; 69 | HIT *hit; 70 | struct timed_circle_struct *bucket; 71 | //dprintf("resetting bucket stats %d\n", r); 72 | 73 | bucket = &tab[r]; 74 | 75 | /* reset statistics for this bucket */ 76 | bucket->time = now; 77 | bucket->bytecount = 0; 78 | bucket->reqcount = 0; 79 | 80 | bucket->rc_summary[2] = 0; 81 | bucket->rc_summary[3] = 0; 82 | bucket->rc_summary[4] = 0; 83 | bucket->rc_summary[5] = 0; 84 | 85 | 86 | /* free up any storage associated with the hit previously in this 87 | * circle position. At the moment this is basically removing a 88 | * refcount from the maps in it */ 89 | for(x = 0 ; x < bucket->hitsize ; x++) 90 | { 91 | hit = &bucket->hits[x]; 92 | if (hit->host_pos) hm->sub_ref(hit->host_pos); 93 | if (hit->url_pos) um->sub_ref(hit->url_pos); 94 | if (hit->ref_pos) rm->sub_ref(hit->ref_pos); 95 | } 96 | 97 | /* start at the beginning of the HIT array */ 98 | bucket->hitpos = 0; 99 | 100 | /* and ensure they're all zero'ed */ 101 | memset(bucket->hits, 0, sizeof(HIT) * bucket->hitsize); 102 | } 103 | 104 | int Timed_Circle::insert(struct logbits lb) 105 | { 106 | short rc_tmp; 107 | 108 | HIT *hit; 109 | struct timed_circle_struct *bucket; 110 | 111 | /* if we're in a new second than the current bucket is for.. */ 112 | // also updates bucketpos 113 | this->garbagecollection(); 114 | 115 | /* for the relevant bucket for this second.. */ 116 | bucket = &tab[bucketpos]; 117 | 118 | /* see if we have a free slot */ 119 | if (bucket->hitpos == bucket->hitsize) 120 | { 121 | /* we don't, increase the size of the hits array */ 122 | bucket->hits = (HIT *) 123 | realloc(bucket->hits, sizeof(HIT) * bucket->hitsize*2); 124 | if (!bucket->hits) 125 | { 126 | dprintf("realloc: %s\n", strerror(errno)); 127 | abort(); /* FIXME: handle a bit better */ 128 | } 129 | 130 | /* clear out the new hit positions */ 131 | memset(&bucket->hits[bucket->hitpos], 132 | 0, sizeof(HIT) * bucket->hitsize); 133 | 134 | bucket->hitsize *= 2; 135 | } 136 | 137 | /* we do (now) */ 138 | hit = &(bucket->hits[bucket->hitpos]); 139 | 140 | 141 | /* free up any storage associated with the hit previously 142 | * in this circle position. At the moment this is basically 143 | * removing a refcount from the maps in it */ 144 | if (hit->host_pos) hm->sub_ref(hit->host_pos); 145 | if (hit->url_pos) um->sub_ref(hit->url_pos); 146 | if (hit->ref_pos) rm->sub_ref(hit->ref_pos); 147 | 148 | /* store the data itself */ 149 | memcpy(hit, &lb, sizeof(lb)); 150 | 151 | /* update stats for this bucket */ 152 | bucket->reqcount++; 153 | bucket->bytecount += lb.bytes; 154 | rc_tmp = (int)lb.retcode/100; 155 | bucket->rc_summary[rc_tmp]++; 156 | 157 | /* ready for our next hitpos */ 158 | bucket->hitpos++; 159 | 160 | return 0; 161 | } 162 | 163 | //int Timed_Circle::walk(unsigned int *url_pos, unsigned int *ip_pos, 164 | // int *bytes, time_t *time, unsigned int *ipl, unsigned int *retcode) 165 | int Timed_Circle::walk(struct logbits **lb) 166 | { 167 | struct timed_circle_struct *bucket; 168 | 169 | *lb = NULL; 170 | 171 | //dprintf("entering walk at bucket %d, hit %d\n", walk_bucketpos, walk_hitpos); 172 | 173 | /* see if we're at the end of this bucket */ 174 | bucket = &tab[walk_bucketpos]; 175 | if (walk_hitpos == bucket->hitpos) 176 | { 177 | //dprintf("end of bucket %d\n", walk_bucketpos); 178 | /* next bucket */ 179 | ++walk_bucketpos; 180 | walk_hitpos = 0; 181 | } 182 | 183 | /* see if we're out of buckets; 184 | * bucketsize-1 is the highest we'll ever use */ 185 | if (walk_bucketpos == MAX_BUCKET) 186 | { 187 | /* done */ 188 | walk_bucketpos = 0; 189 | walk_hitpos = 0; 190 | return -1; 191 | } 192 | 193 | bucket = &tab[walk_bucketpos]; 194 | 195 | /* anything in this bucket? */ 196 | while (walk_bucketpos < MAX_BUCKET && bucket->hitpos == 0) 197 | { 198 | //dprintf("skipping bucket %d\n", walk_bucketpos); 199 | /* try the next one */ 200 | ++walk_bucketpos; 201 | walk_hitpos = 0; 202 | bucket = &tab[walk_bucketpos]; 203 | //dprintf("skipped to bucket %d\n", walk_bucketpos); 204 | 205 | /* see if we're out of buckets; 206 | * bucketsize-1 is the highest we'll ever use */ 207 | if (walk_bucketpos == MAX_BUCKET) 208 | { 209 | /* done */ 210 | walk_bucketpos = 0; 211 | walk_hitpos = 0; 212 | return -1; 213 | } 214 | } 215 | 216 | *lb = &(bucket->hits[walk_hitpos]); 217 | 218 | ++walk_hitpos; 219 | 220 | return 0; 221 | } 222 | 223 | void Timed_Circle::updatestats(void) 224 | { 225 | unsigned int x; 226 | struct timed_circle_struct *bucket; 227 | 228 | // clear any buckets between last hit and now 229 | this->garbagecollection(); 230 | 231 | /* now update stats from all buckets with something in them */ 232 | reqcount = 0; bytecount = 0; 233 | rc_summary[2] = rc_summary[3] = rc_summary[4] = rc_summary[5] = 0; 234 | 235 | for(x = 0, bucket = &tab[0] ; x < bucketsize ; ++x, ++bucket) 236 | { 237 | reqcount += bucket->reqcount; 238 | bytecount += bucket->bytecount; 239 | 240 | rc_summary[2] += bucket->rc_summary[2]; 241 | rc_summary[3] += bucket->rc_summary[3]; 242 | rc_summary[4] += bucket->rc_summary[4]; 243 | rc_summary[5] += bucket->rc_summary[5]; 244 | } 245 | } 246 | 247 | void Timed_Circle::garbagecollection(void) 248 | { 249 | unsigned int x, prevbucket; 250 | 251 | /* clear out the hits from any buckets which we've passed since our 252 | * last garbage collection. Basically, if a period of time passes in 253 | * between hits, we may go from bucket 1 .. 5 and skip 2/3/4. We 254 | * need to empty them here. 255 | */ 256 | 257 | if (tab[bucketpos].time < now) 258 | { 259 | prevbucket = bucketpos; 260 | bucketpos = (now - gstats.start) % bucketsize; 261 | 262 | if (bucketpos < prevbucket) 263 | { 264 | /* we've gone from end of array to start again */ 265 | 266 | /* so first, 0 to wherever */ 267 | for (x = 0 ; x <= bucketpos ; ++x) 268 | resetbucketstats(x); 269 | 270 | /* now from old position to end */ 271 | for (x = prevbucket+1 ; x < MAX_BUCKET ; ++x) 272 | resetbucketstats(x); 273 | } 274 | else 275 | { 276 | /* simpler, just (eg) 7 to 9 */ 277 | for(x = prevbucket+1 ; x <= bucketpos ; ++x) 278 | resetbucketstats(x); 279 | } 280 | 281 | // if we're overrunning, time to loop round 282 | // if (bucketpos == bucketsize) 283 | // bucketpos = 0; // back to first bucket 284 | } 285 | } 286 | 287 | 288 | time_t Timed_Circle::oldest(void) 289 | { 290 | time_t t = now; 291 | unsigned int x; 292 | struct timed_circle_struct *ptr; 293 | 294 | /* this may take up some considerable time with a large circle? */ 295 | for(x = 0, ptr = &tab[0] ; x < MAX_BUCKET ; ++x, ++ptr) 296 | t = getMIN(ptr->time, t); 297 | 298 | return t; 299 | } 300 | -------------------------------------------------------------------------------- /src/timed_circle.h: -------------------------------------------------------------------------------- 1 | #ifndef _TIMED_CIRCLE_H_ 2 | #define _TIMED_CIRCLE_H_ 3 | 4 | /* store hits for a given second in here. 5 | * some bits of the logbits struct aren't used in the circle but 6 | * it's better than making an entirely new struct */ 7 | typedef struct logbits HIT; 8 | 9 | class Timed_Circle : public Circle 10 | { 11 | public: 12 | 13 | int create(unsigned int size); 14 | int insert(struct logbits lb); 15 | int walk(struct logbits **lb); 16 | 17 | void updatestats(void); 18 | time_t oldest(void); 19 | 20 | double getreqcount(void) { return reqcount; } 21 | double getbytecount(void) { return bytecount; } 22 | double getsummary(int r_c) { return rc_summary[r_c]; } 23 | 24 | private: 25 | int initbuckets(const unsigned int from, const unsigned int to); 26 | void resetbucketstats(const unsigned int r); 27 | 28 | void garbagecollection(void); 29 | 30 | double reqcount, bytecount; 31 | double rc_summary[6]; 32 | 33 | unsigned int bucketsize; /* how many buckets (seconds) ? */ 34 | unsigned int bucketpos; /* which bucket are we using now? */ 35 | 36 | /* the timed_circle_struct is a set of buckets; each bucket contains 37 | * data about hits for a given second. Each hit is stored in a 38 | * HIT array inside the relevant bucket */ 39 | struct timed_circle_struct 40 | { 41 | time_t time; /* second this bucket represents */ 42 | 43 | /* stats for the HITs array */ 44 | double reqcount, bytecount; 45 | double rc_summary[6]; 46 | 47 | unsigned int hitsize; /* how big is the array for hits? */ 48 | unsigned int hitpos; /* how far along hits array we are */ 49 | HIT *hits; /* hits for this second go into array */ 50 | 51 | } *tab; 52 | 53 | /* for walk() */ 54 | unsigned int walk_bucketpos; 55 | unsigned int walk_hitpos; 56 | 57 | }; 58 | 59 | #endif 60 | --------------------------------------------------------------------------------