-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathl7logparser.PRG
More file actions
1784 lines (1611 loc) · 63.9 KB
/
l7logparser.PRG
File metadata and controls
1784 lines (1611 loc) · 63.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
* L7LogParser.PRG
#include L7.H
*--- TEST CODE:
#DEFINE test_input_folder [.\Test\]
#define test_maxfiles 0
#define test_start_date {^2010-11-06}
#define test_end_date {^2010-11-06}
LPARAMETERS ldStart, ldEnd, lcInputPath
set procedure to L7Table, L7TableRender, L7Utils, L7Textstream, L7PageElement, L7PageElements, L7Iterator, L7Parsers, L7HtmlLib additive
set procedure to wwUtils, wwAPI, wwSmtp additive
ldStart = EVL(m.ldStart, TEST_START_DATE)
ldEnd = EVL(m.ldEnd, TEST_END_DATE)
lcInputPath = EVL(m.lcInputPath, TEST_INPUT_FOLDER)
CLEAR
LOCAL lcObs, loObservers, laObs[1], loProc, lcObsClass, lcObsKey
loObservers = CREATEOBJECT('Collection')
* Match Morning Report:
WITH loObservers
.Add(CREATEOBJECT('L7TalkLogObserver'), 'Talk')
.Add(CREATEOBJECT('L7UnparseablePathLogObserver'), 'UnparseablePath')
lcObsKey = 'Watched IPs'
.Add(CREATEOBJECT('L7GenericLogObserver'), m.lcObsKey)
.item(m.lcObsKey).lNoteVirtual = .T.
.item(m.lcObsKey).cIPs = "204.68.195.14"
.item(m.lcObsKey).lNoteIP = .T.
.item(m.lcObsKey).lNoteOutputSize = .T.
.item(m.lcObsKey).lConsumeHits = .T.
.item(m.lcObsKey).lGroupHits = .T.
.item(m.lcObsKey).cReportOrder = "IPAddress, OutputSize DESCENDING"
*!* <observer vfpClass="L7GenericLogObserver" active="1">
*!* <param name="cName" value="Watched IPs" />
*!* <param name="cIPs" value="204.68.195.14" />
*!* <param name="lNoteIP" value=".T." />
*!* <param name="lNoteVirtual" value=".T." />
*!* <param name="lNoteOutputSize" value=".T." />
*!* <param name="lConsumeHits" value=".F." />
*!* <param name="lGroupHits" value=".T." />
*!* <param name="cReportOrder" value="IPAddress, OutputSize DESCENDING" />
*!* </observer>
.Add(CREATEOBJECT('L7UnhandledLogObserver'), 'Unhandled')
.item('Unhandled').cName = 'Hits Not "Claimed" By Above Observers'
endwith
*!* * Stakeholder site totals:
*!* WITH loObservers
*!* .Add(CREATEOBJECT('L7TalkLogObserver'), 'Talk')
*!* .Add(CREATEOBJECT('L7UnparseablePathLogObserver'), 'UnparseablePath')
*!* .Add(CREATEOBJECT('L7GenericLogObserver'), 'IgnoreStems')
*!* .item('IgnoreStems').cStatuses = '200'
*!* .item('IgnoreStems').cFilenames = 'robots.txt, favicon.ico'
*!* .item('IgnoreStems').lNoteFilename = .T.
*!* .item('IgnoreStems').lNoteVirtual = .F.
*!* .item('IgnoreStems').lNoteStatus = .F.
*!* .item('IgnoreStems').cName = 'Ignore Specific File Names'
*!* ** .Add(CREATEOBJECT('L7IgnoreStemsLogObserver'), 'OldIgnoreStems')
*!* .Add(CREATEOBJECT('L7GenericLogObserver'), '404Status')
*!* .item('404Status').cStatuses = '404'
*!* .item('404Status').lNoteVirtual = .T.
*!* .item('404Status').lNoteFilename = .T.
*!* .item('404Status').lNoteStatus = .F.
*!* .item('404Status').lNoteFileType = .F.
*!* .item('404Status').cName = '404 Responses'
*!* .item('404Status').lGroupHits = .T.
*!*
*!* .Add(CREATEOBJECT('L7GenericLogObserver'), '400Status')
*!* .item('400Status').cStatuses = '40*, 50*'
*!* .item('400Status').lNoteVirtual = .T.
*!* .item('400Status').lNoteStatus = .T.
*!* .item('400Status').lNoteFileType = .F.
*!* .item('400Status').cName = '400 and 500 Status Responses'
*!* .Add(CREATEOBJECT('L7GenericLogObserver'), 'IgnoreStatuses')
*!* .item('IgnoreStatuses').cStatuses = '206,302,304,307'
*!* .item('IgnoreStatuses').lNoteVirtual = .F.
*!* .item('IgnoreStatuses').lNoteStatus = .T.
*!* .item('IgnoreStatuses').cName = 'Ignore Specific HTTP Status Responses'
*!* .Add(CREATEOBJECT('L7GenericLogObserver'), 'IgnoreFileTypes')
*!* .item('IgnoreFileTypes').cStatuses = '200'
*!* .item('IgnoreFileTypes').cFileTypes = 'jpg,gif,png,jpeg,css,js'
*!* .item('IgnoreFileTypes').lNoteFileType = .T.
*!* .item('IgnoreFileTypes').lNoteVirtual = .F.
*!* .item('IgnoreFileTypes').lNoteStatus = .T.
*!* .item('IgnoreFileTypes').cName = 'Ignore Specific File Types'
*!* *!* .Add(CREATEOBJECT('L7DayAndPageStatsLogObserver'), 'PageStats')
*!* *!* .item('PageStats').cVirtuals = ',comm,'
*!* *!* .item('PageStats').cName = 'Stakeholder Site Page Counts'
*!* .Add(CREATEOBJECT('L7GenericLogObserver'), 'OtherVirtuals')
*!* .item('OtherVirtuals').cStatuses = '200'
*!* .item('OtherVirtuals').cVirtuals = NULL
*!* .item('OtherVirtuals').lNoteVirtual = .T.
*!* .item('OtherVirtuals').cName = 'Other Unhandled Virtual Paths'
*!* .Add(CREATEOBJECT('L7GenericLogObserver'), 'OtherHits')
*!* .item('OtherHits').lNoteStatus = .T.
*!* .item('OtherHits').lNoteVirtual = .T.
*!* .item('OtherHits').cName = 'Other Hits'
*!* .Add(CREATEOBJECT('L7UnhandledLogObserver'), 'Unhandled')
*!* ** .item('Unhandled').cName = 'Hits Not "Claimed" By Above Observers'
*!* ENDWITH
loProc = CREATEOBJECT('L7LogParser')
WITH loProc
.SetObservers(m.loObservers)
.SetLogFilePath(m.lcInputPath)
.nMaxFiles = test_maxfiles
.dStartDate = m.ldStart
.dEndDate = m.ldEnd
.Parse()
SET MEMOWIDTH TO 100
? .cReport + .cWarnings
_CLIPTEXT = .cReport
ENDWITH
RELEASE loProc
SET PROCEDURE TO
CLEAR PROGRAM
CLEAR ALL
RETURN
*--- end: TEST CODE.
*** ========================================================= ***
DEFINE CLASS L7LogParser as Custom
cContentType = "text/html"
lHTML = null && access mthd
cLogFilePath = NULL && where to look for web server logs
nMaxFiles = 0 && if >0, ceases processing after N files
nFilesProcessed = 0
* Date range of files to process:
dStartDate = NULL && if not NULL, restricts date range
dEndDate = NULL && same; suggest setting to avoid active log file!
* Overall counts and status properties:
nDirectives = 0
cDirectives = ''
nHits = 0 && hits processed current file
nTotalHits = 0 && total hits processed; observers can also maintain their own counts
cSummary = ""
cReport = ""
cWarnings = ""
nWarnings = 0
* Current file being processed:
cCurrentFile = ''
dCurrentDate = {}
cCurrentFields = ''
lFileBlocked = .F. && observer can block a file in response to WouldProcessFile event
*
cFieldPattern = ''
*
lLineConsumed = .F. && observer sets this to "eat" (claim) the hit
cCurrentLine = ''
cPreviousLine = ''
* Members and other objects:
oCurrentHit = NULL
oPreviousHit = NULL
ADD OBJECT Fields as Collection
ADD OBJECT RegExps AS Collection
* --------------------------------------------------------- *
function lHTML_ACCESS
return this.cContentType == "text/html"
endfunc
* --------------------------------------------------------- *
FUNCTION init
DECLARE Integer Sleep IN WIN32API
LOCAL loRE
loRE = CREATEOBJECT(L7_REGEXP_CLASS)
loRE.global = .T.
loRE.Pattern = '([^\s]+)(?:\s|$)'
THIS.RegExps.Add(m.loRE, "FieldList")
THIS.RegExps.Add(CREATEOBJECT(L7_REGEXP_CLASS), "SingleHit")
* hit pattern is created each time field directives are found
THIS.CreateHitObjects()
ENDFUNC
* --------------------------------------------------------- *
FUNCTION CreateHitObjects()
THIS.oCurrentHit = CREATEOBJECT('L7LogParserHit')
THIS.oPreviousHit = CREATEOBJECT('L7LogParserHit')
ENDFUNC
* --------------------------------------------------------- *
FUNCTION SetLogFilePath(lcPath)
THIS.cLogFilePath = ADDBS(m.lcPath)
ENDFUNC
* --------------------------------------------------------- *
FUNCTION SetObservers(loColl)
LOCAL loObs, lnItem
** FOR EACH loObs IN loColl && cannot use because of FOXOBJECT bug!!
FOR lnItem = 1 TO loColl.Count
loObs = loColl.item(m.lnItem)
loObs.Setup(this)
ENDFOR
ENDFUNC
* --------------------------------------------------------- *
FUNCTION Parse
RAISEEVENT(this, 'WouldProcessFiles', this)
LOCAL lnFiles, laFiles[1], lcFile, ldDate
THIS.nFilesProcessed = 0
lnFiles = ADIR(laFiles, this.cLogFilePath + [*.log])
? lnFiles, "total files in path"
FOR lnFile = 1 TO m.lnFiles
lcFile = laFiles(m.lnFile, 1)
TRY
ldDate = DATE(2000 + VAL(SUBSTR(lcFile,3,2)), VAL(SUBSTR(lcFile,5,2)), VAL(SUBSTR(lcFile,7,2)))
CATCH
ldDate = NULL
ENDTRY
IF ISNULL(m.ldDate) OR EMPTY(m.ldDate)
LOOP
ENDIF
IF (NOT ISNULL(this.dStartDate) AND m.ldDate < this.dStartDate) OR ;
(NOT ISNULL(this.dEndDate) AND m.ldDate > this.dEndDate)
LOOP
ENDIF
THIS.dCurrentDate = m.ldDate
THIS.nFilesProcessed = THIS.nFilesProcessed + 1
this.cCurrentFile = m.lcFile
this.lFileBlocked = .F. && clear from any previous setting
RAISEEVENT(this, 'WouldProcessFile', this)
IF NOT THIS.lFileBlocked && allows an observer to block a file
this.ProcessFile(m.lcFile)
this.nTotalHits = this.nTotalHits + THIS.nHits && add file hits to total hits
RAISEEVENT(THIS, 'DidProcessFile', this)
ENDIF
IF this.nMaxFiles > 0 AND THIS.nFilesProcessed >= this.nMaxFiles
EXIT
ENDIF
TRY
Sleep(500) && ms between files
CATCH
ENDTRY
ENDFOR
RAISEEVENT(this, 'DidProcessFiles', this)
THIS.cReport = THIS.cReport + CRLF + ;
TEXTMERGE([<<TRANSFORM(THIS.nTotalHits, "999,999,999")>> TOTAL HITS PROCESSED.]) + CRLF + ;
TEXTMERGE([<<TRANSFORM(THIS.nFilesProcessed, "999,999,999")>> DAILY LOG FILES PROCESSED.]) + CRLF
RETURN
ENDFUNC
* --------------------------------------------------------- *
FUNCTION AddWarning(lcMsg)
THIS.nWarnings = THIS.nWarnings + 1
THIS.cWarnings = THIS.cWarnings + m.lcMsg + CRLF
RETURN
ENDFUNC
* --------------------------------------------------------- *
FUNCTION WouldProcessFiles(loSelf)
ENDFUNC
FUNCTION DidProcessFiles(loSelf)
ENDFUNC
FUNCTION OnFileOpenFailure(loSelf)
WITH loSelf
THIS.AddWarning("Could not open file for processing: " + .cCurrentFile)
ENDWITH
RETURN
ENDFUNC
FUNCTION WouldProcessFile(loSelf)
ENDFUNC
FUNCTION DidProcessFile(loSelf)
ENDFUNC
FUNCTION WouldProcessDirectives(loSelf)
ENDFUNC
FUNCTION DidProcessDirectives(loSelf)
ENDFUNC
FUNCTION WouldParseFields(loSelf)
ENDFUNC
FUNCTION WouldProcessHits(loSelf)
ENDFUNC
* --------------------------------------------------------- *
FUNCTION ProcessFile(lcFile)
LOCAL nn, hh, ss, llDirecs, llInDirecs
STORE 0 TO THIS.nHits, THIS.nDirectives
nn = 0
hh = FOPEN(this.cLogFilePath + m.lcFile, 0)
IF m.hh > 0
DO WHILE NOT FEOF(m.hh)
ss = FGETS(m.hh, 8192)
nn = m.nn + 1
IF m.nn % 100 = 0
TRY
Sleep(20) && ms every 100 lines [[PARAMETERIZE THIS]]
CATCH
ENDTRY
ENDIF
IF EMPTY(m.ss) && are blank lines allowed per spec? assume yes...
LOOP
ENDIF
IF m.ss = "#" && directive, rather than hit
llDirecs = .T.
IF NOT m.llInDirecs
RAISEEVENT(this, 'WouldProcessDirectives', this)
this.cDirectives = ""
llInDirecs = .T. && flag indicates we're in the middle of
* processing directives, which occur over several lines
ENDIF
THIS.nDirectives = THIS.nDirectives + 1
this.cDirectives = this.cDirectives + m.ss + CRLF
IF LOWER(ALLTRIM(GETWORDNUM(m.ss, 1, ":"))) == "#fields"
this.cCurrentFields = GETWORDNUM(m.ss, 2, ":")
RAISEEVENT(this, 'WouldParseFields', this)
this.ParseFields()
ENDIF
ELSE
IF NOT m.llDirecs
ERROR "No log file directive lines in file: " + m.lcFile
ENDIF
IF m.llInDirecs && last line was a directive, this is first hit
RAISEEVENT(this, 'DidProcessDirectives', this)
this.WouldProcessHits(this)
llInDirecs = .F.
ENDIF
this.cCurrentLine = m.ss
this.lLineConsumed = .F.
RAISEEVENT(this, 'readHit', this)
THIS.nHits = THIS.nHits + 1
ENDIF
ENDDO
FCLOSE(m.hh)
? lcFile, nn, "lines in file"
ELSE
RAISEEVENT(this, 'OnFileOpenFailure', this)
ENDIF
RETURN
ENDFUNC
* --------------------------------------------------------- *
FUNCTION ParseFields()
LOCAL loRE, lnMatches, loMatches, loMatch, lcField, lcFldPattern
lcFldPattern = ""
this.Fields.Remove(-1)
loRE = THIS.RegExps("FieldList")
loMatches = loRE.Execute(this.cCurrentFields)
lnMatches = loMatches.Count
FOR EACH loMatch IN loMatches
**loMatch = loMatches.item(0)
lcField = ALLTRIM(loMatch.value)
this.Fields.add(m.lcField, m.lcField)
** ? m.lcField
lcFldPattern = m.lcFldPattern + "\s*([^\s]+)"
ENDFOR
IF NOT m.lcFldPattern == THIS.RegExps("SingleHit").Pattern
THIS.RegExps("SingleHit").Pattern = m.lcFldPattern
** ? this.cCurrentFields
** ? m.lcFldPattern
THIS.cFieldPattern = m.lcFldPattern
ENDIF
** ? lnMatches, loMatch.Value
RETURN .T.
ENDFUNC
* --------------------------------------------------------- *
FUNCTION ReadHit(loSelf)
EXTERNAL ARRAY loFlds
LOCAL loFlds, lnField, lcField, lcVal, loRE, loMatches, loTemp, loExc
loFlds = this.Fields
loRE = THIS.RegExps('SingleHit')
loMatches = loRE.Execute(THIS.cCurrentLine)
IF loMatches.Count <> 1
RAISEEVENT(this, "onBadLine", this, "noMatch")
ELSE
loMatch = loMatches.item(0)
IF loMatch.Submatches.Count <> THIS.Fields.Count
RAISEEVENT(this, "onBadLine", this, "badFieldCount")
ELSE
* swap objects, in case an observer wants to see previous hit
loTemp = this.oPreviousHit
this.oPreviousHit = this.oCurrentHit
this.oCurrentHit = m.loTemp
WITH this.oCurrentHit
.Reset()
FOR lnField = 1 TO loFlds.Count
lcField = loFlds[m.lnField]
lcVal = loMatch.Submatches(m.lnField - 1)
DO CASE
CASE m.lcField == "date" && yyyy-mm-dd
.dDate = CTOD([^] + m.lcVal)
CASE m.lcField == "time" && nn:nn:nn
.cTime = m.lcVal
.nHour = VAL(LEFT(.cTime, 2))
CASE m.lcField == "sc-status"
.nStatus = VAL(m.lcVal)
CASE m.lcField == "c-ip"
.cClientIp = m.lcVal
TRY
.cNetwork = IpToNetwork(.cClientIp)
CATCH
ENDTRY
CASE m.lcField == "cs(Referer)"
.cReferer = IIF(m.lcVal == "-", "", m.lcVal)
CASE m.lcField == "cs-method"
.cMethod = m.lcVal
CASE m.lcField == "time-taken"
.nDuration = VAL(m.lcVal) / 1E+3 && store in seconds, not ms
CASE m.lcField == "sc-bytes"
.nOutputSize = VAL(m.lcVal)
CASE m.lcField == "cs-bytes"
.nInputSize = VAL(m.lcVal)
CASE m.lcField == "cs-uri-stem"
* NOTE: Need a general review of where LOWER() is used (throughout this module),
* and whether there is need for a case-sensitive switch.
.cLogicalPath = m.lcVal
TRY
.cExtension = LOWER(JUSTEXT(m.lcVal))
.cStem = LOWER(JUSTSTEM(m.lcVal))
.cPath = LOWER(JUSTPATH(m.lcVal))
CATCH
* some hacks create strings that cause errors in path functions
* ex: <script> embedded in URL
ENDTRY
CASE m.lcField == "cs-uri-query"
.cQueryString = m.lcVal
CASE m.lcField == "cs(User-Agent)"
.cUserAgent = m.lcVal
CASE m.lcField == "cs(Cookie)"
.cCookie = m.lcVal
CASE m.lcField == "cs-username"
.cUserName = m.lcVal
ENDCASE
ENDFOR
ENDWITH
ENDIF
ENDIF
RETURN
ENDFUNC
* --------------------------------------------------------- *
FUNCTION onBadLine(loSelf, lcReason)
RETURN
ENDFUNC
* --------------------------------------------------------- *
FUNCTION statusMessage(loObs, lcMessage, llVerbose)
RETURN
ENDFUNC
* --------------------------------------------------------- *
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7LogParserHit as Line
*
* CAUTION: When adding any property here, BE CERTAIN to
* add a mathcing line to the Reset() method (object reuse).
*
dDate = NULL
cTime = NULL
nHour = NULL
nStatus = NULL
cReferer = NULL
cClientIp = NULL
cNetwork = NULL && calc from IP
nDuration = NULL
nOutputSize = NULL
nInputSize = NULL
cMethod = NULL
cLogicalPath = NULL
cExtension = NULL && calc
cStem = NULL && calc
cPath = NULL && calc
cQueryString = NULL
cUserAgent = NULL
cUserName = NULL
cCookie = NULL
* ---------------------------------------- *
FUNCTION Reset
WITH this
.dDate = NULL
.cTime = NULL
.nHour = NULL
.nStatus = NULL
.cReferer = NULL
.cClientIp = NULL
.cNetwork = NULL
.nDuration = NULL
.nOutputSize = NULL
.nInputSize = NULL
.cMethod = NULL
.cLogicalPath = NULL
.cExtension = NULL
.cStem = NULL
.cPath = NULL
.cQueryString = NULL
.cUserAgent = NULL
.cUserName = NULL
.cCookie = NULL
ENDWITH
ENDFUNC
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7TalkLogObserver AS L7LogObserver
* "TALK" observer relays status messages to screen. Used
* when running from an IDE during development/testing.
* Place anywhere in chain.
lCountHits = .F.
lConsumeHits = .F.
* --------------------------------------------------------- *
FUNCTION setup(loParser)
BINDEVENT(m.loParser, 'StatusMessage', THIS, 'echoStatusMessage')
ENDFUNC
* --------------------------------------------------------- *
FUNCTION echoStatusMessage(loObs, lcMessage, llVerbose)
? loObs.Class + ":", m.lcMessage
ENDFUNC
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7UnparseablePathLogObserver as L7LogObserver
nReportHitThreshhold = 1 && don't report if no hits
* --------------------------------------------------------- *
FUNCTION setup(loParser)
BINDEVENT(m.loParser, 'ReadHit', THIS, 'CheckStatus', 3)
BINDEVENT(m.loParser, 'DidProcessFiles', THIS, 'NoteFilesEnd')
ENDFUNC
* --------------------------------------------------------- *
FUNCTION CheckStatus(loParser)
WITH loParser
IF .lLineConsumed = .T.
RETURN
ENDIF
IF ISNULL(.oCurrentHit.cExtension) OR ISNULL(.oCurrentHit.cPath) OR ISNULL(.oCurrentHit.cStem)
* Error parsing out uri-stem (hack!): most other parsers would fail.
.lLineConsumed = .T. && claim hit and eat
this.nHits = this.nHits + 1
ENDIF
ENDWITH
ENDFUNC
ENDDEFINE && L7UnparseablePathLogObserver
*** ========================================================= ***
DEFINE CLASS L7UnhandledLogObserver as L7LogObserver
* "Unhandled Hit" observer: operates on all hits that aren't
* consumed by anything else; can optionally consume the
* hit, or just count things going by.
* Typically placed at end of observer chain.
cName = 'Hits Not "Claimed" By Above Observers'
lConsumeHits = .F. && if .T., also consumes the hit
lCountHits = .T. && if .T. (default), counts the hit
lNoteLogicalPaths = .F. && if .T., records distinct logical paths
* --------------------------------------------------------- *
FUNCTION setup(loParser)
BINDEVENT(m.loParser, 'ReadHit', THIS, 'CheckStatus', 3)
BINDEVENT(m.loParser, 'DidProcessFiles', THIS, 'NoteFilesEnd')
THIS.cAlias = THIS.Class + [_] + SYS(2015)
SELECT 0
CREATE CURSOR (THIS.cAlias) (PageCRC C(10), Hits I, Page C(150))
INDEX ON PageCRC TAG PageCRC
ENDFUNC
* --------------------------------------------------------- *
FUNCTION CheckStatus(loParser)
IF loParser.lLineConsumed = .F.
IF THIS.lConsumeHits
loParser.lLineConsumed = .T.
ENDIF
IF THIS.lCountHits
THIS.nHits = THIS.nHits + 1
IF THIS.lNoteLogicalPaths
LOCAL lcVirtual, loHit, lcCrc
loHit = loParser.oCurrentHit
lcCRC = SYS(2007, loHit.cLogicalPath, -1, 1)
SELECT (THIS.cAlias)
LOCATE FOR PageCRC = m.lcCRC
IF NOT FOUND()
APPEND BLANK
REPLACE PageCRC WITH m.lcCRC, Page WITH loHit.cLogicalPath
ENDIF
REPLACE Hits WITH Hits + 1
ENDIF
ENDIF
** INSERT INTO (THIS.cAlias) VALUES (loParser.oCurrentHit.cLogicalPath)
ENDIF
ENDFUNC
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7CountLogObserver as L7LogObserver
* --------------------------------------------------------- *
FUNCTION setup(loParser)
BINDEVENT(m.loParser, 'WouldProcessFile', THIS, 'NoteFileStart')
BINDEVENT(m.loParser, 'DidProcessFile', THIS, 'NoteFileEnd')
ENDFUNC
* --------------------------------------------------------- *
FUNCTION NoteFileStart(loParser)
** ? loParser.cCurrentFile, loParser.dCurrentDate
RAISEEVENT(loParser, "StatusMessage", THIS, ;
TRANSFORM(loParser.cCurrentFile) + ' ' + TRANSFORM(loParser.dCurrentDate))
ENDFUNC
* --------------------------------------------------------- *
FUNCTION NoteFileEnd(loParser)
** ? loParser.nDirectives, loParser.nHits
RAISEEVENT(loParser, "StatusMessage", THIS, TRANSFORM(loParser.nHits))
ENDFUNC
ENDDEFINE
*** Next few classes are property-only subclasses of L7GenericLogObserver.
*** These are included to allow easy insertion of "standard" observer needs
*** from XML config, without needing to specify lots of property overrides.
*** ========================================================= ***
DEFINE CLASS L7BrokenLinksLogObserver AS L7GenericLogObserver
cName = "Broken Links Observer (Refered 400-Status Hits)"
lConsumeHits = .T. && subsequent observers should ignore hits
cStatuses = "404"
lNoteLogicalPath = .T. && this instead, so we see path/directory issues
lNoteReferer = .T.
lRefererEmpty = .F.
lGroupHits = .T. && .F. to list hits individually
cReportOrder = "LogicPath"
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7IgnoreForbiddenLogObserver AS L7GenericLogObserver
cName = 'FILTER Forbidden Requests (403 Status)'
lConsumeHits = .T. && subsequent observers should ignore hits
cStatuses = '403'
lNoteIP = .T.
lNoteDate = .T.
lNoteMethod = .T.
lGroupHits = .T. && counts hits by IP
cReportOrder = "IPAddress, HitDate, Method"
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7ServerErrorsLogObserver AS L7GenericLogObserver
cName = "FILTER Server Errors (500-series HTTP Status) Hits"
lConsumeHits = .T.
cStatuses = "50*"
lNoteStatus = .T.
lNoteMethod = .T.
lNoteVirtual = .T.
lNoteFilename = .T.
lGroupHits = .T.
cReportOrder = "Virtual, Status, Method, Filename"
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7BadStatusFilenamesLogObserver AS L7GenericLogObserver
cName = "FILTER 400-500 Status Hits"
lConsumeHits = .T.
cStatuses = "40*,50*"
lNoteStatus = .T.
lNoteMethod = .T.
lNoteVirtual = .T.
lNoteFilename = .T.
lGroupHits = .T.
cReportOrder = "Virtual, Status, Method, Filename"
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7CountByFilenamesLogObserver AS L7GenericLogObserver
cName = "Hit Counts by Filename"
lConsumeHits = .F. && can override
cStatuses = "200"
lNoteStatus = .F.
lNoteLogicalPath = .T.
lGroupHits = .T.
cReportOrder = "LogicPath"
* To use this as an observer for a *single* virtual, change to:
* cVirtuals = "/myVirtual" (example)
* cReportOrder = "Hits DESCENDING"
* nReportHitThreshhold = 10 (optional, example)
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7IgnoreCommonTypesLogObserver AS L7GenericLogObserver
cName = 'FILTER Specific File Extensions'
lConsumeHits = .T. && subsequent observers should ignore hits
cStatuses = '200,304' && OK or Not Modified
cFileTypes = 'jpg,gif,png,jpeg,css,js'
lNoteFileType = .T.
lNoteVirtual = .F.
lNoteStatus = .F.
lGroupHits = .T. && counts hits by type
cReportOrder = "FileType"
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7IgnoreFilesLogObserver AS L7GenericLogObserver
cName = 'FILTER Specific Generic Files'
lConsumeHits = .T. && subsequent observers should ignore hits
cFilenames = 'robots.txt, favicon.ico'
lNoteFilename = .T.
lNoteVirtual = .F.
lNoteStatus = .F. && status-neutral to cover policies against robots.txt that produce 403's
lGroupHits = .T. && counts hits by filename
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7CountByVirtualLogObserver AS L7GenericLogObserver
cName = "Hit Counts by Virtual Directory"
lConsumeHits = .F. && subsequent observers also will examine hit
lNoteStatus = .F.
cStatuses = "200" && only count delivered responses (OK)
lNoteVirtual = .T.
cReportOrder = "Virtual, Hits DESCENDING"
lGroupHits = .T.
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7BrowserDistributionLogObserver AS L7GenericLogObserver
cName = "Browser Distribution (>= 50 hits)"
lConsumeHits = .F. && subsequent observers also will examine hit
lNoteStatus = .F.
* cStatuses = "200" && only count delivered responses (OK)
lNoteUA = .T.
cReportOrder = "Hits DESCENDING"
nReportHitThreshhold = 50
lGroupHits = .T.
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7CountByStatusLogObserver AS L7GenericLogObserver
cName = "Hit Counts by HTTP Status"
lConsumeHits = .F. && subsequent observers also will examine hit
lNoteStatus = .T.
cReportOrder = "Status"
lGroupHits = .T.
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7CountByNetworkLogObserver AS L7GenericLogObserver
cName = "Hit Counts by Client Network (>= 10 hits)"
lConsumeHits = .F. && subsequent observers also will examine hit
lNoteNetwork = .T.
lNoteStatus = .F.
cReportOrder = "Hits DESCENDING"
lGroupHits = .T.
nReportHitThreshhold = 10
ENDDEFINE
*** ========================================================= ***
DEFINE CLASS L7GenericLogObserver AS L7LogObserver
lNoteHour = .F.
lNoteDate = .F.
lNoteTime = .F.
cTimeStart = NULL
cTimeEnd = NULL
cVirtuals = NULL && comma-delim list to request specific virtuals
lNoteVirtual = .F.
cStatuses = NULL && often you'll want to set at "200"
lNegateStatuses = .F.
lNoteStatus = .F. && had been .T. by default, changed for consistency
cNetworks = NULL
lNoteNetwork = .F.
cIPs = NULL
lNoteIP = .F.
cUAs = NULL
lNoteUA = .F.
cMethods = NULL
lNegateMethods = .F.
lNoteMethod = .F.
cFileTypes = NULL
lNoteFileType = .F.
cStems = NULL
lNoteStem = .F.
cFilenames = NULL
lNoteFilename = .F.
cPaths = NULL
lNegatePaths = .F. && if T, all *but* cPaths chosen
lNotePath = .F.
cLogicalPaths = NULL
lNoteLogicalPath = .F.
lNoteReferer = .F.
lRefererEmpty = NULL && .T. for empty, .F. for not empty
cReferers = NULL
nMinDuration = NULL && filter for long requests
lNoteDuration = .F.
nMinInputSize = NULL && filter for large requests
lNoteInputSize = .F.
nMinOutputSize = NULL && filter for large responses
lNoteOutputSize = .F.
cReportOrder = NULL && if not null, order results by this
nReportTopN = NULL && if not null, report only TOP n hits
nReportHitThreshhold = NULL && if not null, report all grouped items with N+ hits
*[[ need threshhold for output size too
lNoteHits = .T. && .F. to record each hit
lGroupHits = .T. && .T. for grouping / .F. to list hits individually (applies only if lNoteHits is true)
lGraphHits = .F. && (applies only if lNoteHits *and* lGraphHits are true)
lCountHits = .T. && if .T., counts the hit
lConsumeHits = .T. && if .T., also consumes the hit
* --------------------------------------------------------- *
function setup(loParser)
BINDEVENT(m.loParser, 'ReadHit', THIS, 'CheckStatus', 3)
BINDEVENT(m.loParser, 'DidProcessFiles', THIS, 'NoteFilesEnd')
local lcStr
with this
* if this observer notes hits, build a CURSOR for recording temp data
if .lNoteHits
THIS.cAlias = THIS.Class + "_" + SYS(2015)
lcStr = ""
IF .lGroupHits
lcStr = m.lcStr + [, Hits I]
ENDIF
IF .lNoteDuration
lcStr = m.lcStr + [, Duration N(12,3)]
ENDIF
IF .lNoteInputSize
lcStr = m.lcStr + [, InputSize N(12,0)]
ENDIF
IF .lNoteOutputSize
lcStr = m.lcStr + [, OutputSize N(12,0)]
ENDIF
IF .lNoteDate
lcStr = m.lcStr + [, HitDate D]
ENDIF
IF .lNoteTime
lcStr = m.lcStr + [, HitTime C(8)]
ENDIF
IF .lNoteHour
lcStr = m.lcStr + [, HitHour N(2,0)]
ENDIF
IF .lNoteStatus
lcStr = m.lcStr + [, Status N(3, 0)]
ENDIF
IF .lNoteNetwork
lcStr = m.lcStr + [, Network C(15)]
ENDIF
IF .lNoteIP
lcStr = m.lcStr + [, IPAddress C(15)]
ENDIF
IF .lNoteUA
lcStr = m.lcStr + [, UserAgent C(80)]
ENDIF
IF .lNoteMethod
lcStr = m.lcStr + [, Method C(6)]
ENDIF
IF .lNoteVirtual
lcStr = m.lcStr + [, Virtual C(24)]
ENDIF
IF .lNoteStem
lcStr = m.lcStr + [, Stem C(120)]
ENDIF
IF .lNoteLogicalPath
lcStr = m.lcStr + [, LogicPath C(150)]
ENDIF
IF .lNotePath
lcStr = m.lcStr + [, Path C(60)]
ENDIF
IF .lNoteFileType
lcStr = m.lcStr + [, FileType C(8)]
ENDIF
IF .lNoteFilename
lcStr = m.lcStr + [, Filename C(120)]
ENDIF
IF .lNoteReferer
lcStr = m.lcStr + [, Referer C(150)]
ENDIF
lcStr = SUBSTR(m.lcStr, 1 + LEN([, ]))
CREATE CURSOR (THIS.cAlias) (&lcStr)
endif
endwith
return
endfunc && Setup
*!* FIELD TYPE FILTERING CAPTURE FLAG
*!* ========= ===== ================================== ================
*!* Hits I lCountHits lConsumeHits
*!* lGroupHits lUniqueHits
*!* HitDate D lNoteDate
*!* HitTime C(8) cTimeStart/cTimeEnd lNoteTime
*!* HitHour N(2) lNoteHour
*!* Status N(3) cStatuses lNoteStatus
*!* Network C(15) cNetworks lNoteNetwork
*!* IPAddress C(15) cIPs lNoteIP
*!* UserAgent C(80) cUAs lNoteUA
*!* Method C(6) cMethods lNoteMethod
*!* Virtual C(24) cVirtuals lNoteVirtual
*!* Stem C(120) cStems lNoteStem
*!* LogicPath C(15) cLogicalPaths lNoteLogicalPath
*!* Path C(60) cPaths lNotePath
*!* FileType C(8) cFileTypes lNoteFileType
*!* FileName C(120) cFileNames lNoteFilename
*!* Referer C(150) lRefererEmpty lNoteReferer
* --------------------------------------------------------- *
FUNCTION CheckStatus(loParser)
WITH loParser
IF .lLineConsumed = .T.
RETURN
ENDIF
LOCAL lcLike, llMine, lcLocate, loHit, ;
lcNetwork, lcIp, lcUA, lcMethod, ;
lcLogicalPath, lcPath, lcVirtual, ;
ldHitDate, lcHitTime, lnHitHour, lnStatus, ;
lcFileType, lcStem, lcFilename, lcReferer, ;
lnDuration, lnInputSize, lnOutputSize, ;
llInsertFailed, lcInsertFlds, lcInsertVals, lcGroupReplace, ;
lcCmd, lnWord, loExc, loExc2, llDebug, lcErrMsg
llMine = .T. && flag to consume hit: defaults to true (consume) unless contra-indicated
loHit = .oCurrentHit
* --- Load/interpret hit information into variables:
* [[This looks like a service the parser should provide, otherwise each observer is repeating the
* [[cleaning steps below for every hit.
* For the next 3 items, we'll use -1 if not logged. This avoids type mismatches
* or need to store NULLs in data structures. Analysis routines will need
* to recognize meaning of -1.
lnDuration = NVL(loHit.nDuration, -1)
lnInputSize = NVL(loHit.nInputSize, -1)
lnOutputSize = NVL(loHit.nOutputSize, -1)
lnStatus = loHit.nStatus
ldHitDate = loHit.dDate
lcHitTime = loHit.cTime
lnHitHour = loHit.nHour
lcIP = loHit.cClientIp
lcUA = NVL(loHit.cUserAgent, "(user-agent not logged)")
lcUA = CHRTRAN(m.lcUA, ['"], [||]) && avoid issues with INSERT
IF LEN(m.lcUA) > 150
lcUA = LEFT(m.lcUA, 147) + "..."
ENDIF
lcNetwork = loHit.cNetwork
lcMethod = loHit.cMethod
lcLogicalPath = loHit.cLogicalPath
lcLogicalPath = CHRTRAN(m.lcLogicalPath, ['"], [||]) && avoid issues with INSERT
lcPath = loHit.cPath
lcPath = CHRTRAN(m.lcPath, ['"], [||]) && avoid issues with INSERT
lcFileType = LOWER(loHit.cExtension)
lcStem = LOWER(loHit.cStem)
lcStem = CHRTRAN(m.lcStem, ['"], [||]) && avoid issues with INSERT
lcFileName = FORCEEXT(m.lcStem, m.lcFileType)
** lcFileName = CHRTRAN(m.lcFileName, ['"], [||]) && avoid issues with INSERT
IF LEN(m.lcFileName) > 120
lcFileName = LEFT(m.lcFileName, 117) + "..."
ENDIF
lcReferer = LOWER(NVL(loHit.cReferer, "(referer not logged)"))
lcReferer = CHRTRAN(m.lcReferer, ['"], [||]) && avoid issues with INSERT
IF LEN(m.lcReferer) > 150
lcReferer = LEFT(m.lcReferer, 147) + "..."
ENDIF
lcVirtual = m.lcPath
IF OCCURS("/", m.lcVirtual) > 1
lcVirtual = LEFT(m.lcVirtual, -1 + AT("/", m.lcVirtual, 2))
ENDIF
* --- Determine of hit is of interest ("mine"):
IF m.llMine AND NOT ISNULL(THIS.nMinDuration) && long-running filter
llMine = m.lnDuration >= THIS.nMinDuration