Class ::nsmcp::BoundedLogScanner (public)
::nx::Class ::nsmcp::BoundedLogScanner
Defined in /usr/local/ns/tcl/nsmcp/log-01-scanner.tcl
- Testcases:
- No testcase defined.
Source code: :property cacheDirectory:required :property literals:required :property {channel {}} :property {limit {}} :property {blockBytes:integer 4194304} :variable keys {} :variable overlap 0 :variable buffer {} :variable bufferFrom 0 :variable bufferEnd 0 :variable windowCeiling -1 :variable windowHits 0 :variable readBytes 65536 :variable readMicroseconds 0 :variable sourceBytes 0 :variable readCalls 0 :method init {} { if {[llength ${:literals}]<1 || [llength ${:literals}]>20} { error "scanner requires 1..20 literals" } if {${:blockBytes}<1 || ${:blockBytes}>8388608} { error "invalid scanner buffer limits" } foreach literal ${:literals} { set bytes [encoding convertto utf-8 $literal] set length [string length $bytes] if {$length<1 || $length>4096 || [regexp {[\x00\r\n]} $literal]} { error "scanner literals must be 1..4096 UTF-8 bytes without CR/LF/NUL" } lappend :keys $bytes set :overlap [expr {max(${:overlap},$length-1)}] } set :blockBytes [expr {max(${:blockBytes},${:overlap}+1)}] set :readBytes [expr {min(${:blockBytes},65536)}] if {${:channel} ne "" && (![string is entier -strict ${:limit}] || ${:limit}<0)} { error "scanner requires a fixed nonnegative byte limit" } } # A caller can supply an already bounded, record-aligned byte chunk instead # of a source channel. No source paths or raw bodies enter persistent state. :public method load {bytes} { # # Load a bounded in-memory byte chunk for candidate scanning. # # Reject channel-backed scanners and chunks larger than the configured block # bound. # # @param bytes Raw binary chunk, no larger than the configured block limit. # # @return The new in-memory byte limit. # if {${:channel} ne ""} { error "load requires an in-memory scanner" } if {[string length $bytes]>${:blockBytes}} { error "scanner chunk exceeds its byte bound" } :setBuffer $bytes 0 [string length $bytes] set :limit ${:bufferEnd} } :method setBuffer {bytes from ceiling} { set :buffer $bytes set :bufferFrom $from set :bufferEnd [expr {$from+[string length $bytes]}] set :windowCeiling $ceiling set :windowHits 0 } :method first {position ceiling} { set relative [expr {$position-${:bufferFrom}}] set available [expr {$ceiling-${:bufferFrom}}] set found -1 # Search the existing bytes directly. A scratch file adds writes and # descriptor operations without improving the bounded search contract. set buffer ${:buffer} foreach key ${:keys} { set candidate [string first $key $buffer $relative] if {$candidate>=0 && $candidate+[string length $key]<=$available && ($found<0 || $candidate<$found)} { set found $candidate } } if {$found>=0} { return [expr {${:bufferFrom}+$found}] } return -1 } :public method statistics {} { # # Report source-channel work performed by this scanner. # # Counts describe Tcl channel reads, including overlapping windows, not # physical disk operations. In-memory scanning performs no channel I/O. # # @return A dictionary with readMicroseconds, readBytes and readCalls. # return [dict create readMicroseconds ${:readMicroseconds} readBytes ${:sourceBytes} readCalls ${:readCalls}] } # Return the earliest raw match fully contained in [from, ceiling). The # ceiling can shrink between calls; buffered bytes beyond it never qualify. :public method find {from {ceiling {}}} { # # Find the earliest candidate within a fixed byte interval. # # Use overlapping bounded windows for channels or the current memory chunk; # never read beyond the ceiling. # # @param from Inclusive starting byte offset within the fixed scan interval. # @param ceiling Exclusive upper byte bound; empty uses the configured # scanner limit. # # @return The candidate byte offset, or -1 when no candidate is found. # if {$ceiling eq ""} { set ceiling ${:limit} } if {![string is entier -strict $from] || ![string is entier -strict $ceiling] || $from<0 || $ceiling<$from || $ceiling>${:limit}} { error "invalid scanner byte interval" } set position $from while {$position<$ceiling} { if {${:channel} eq ""} { set found [:first $position $ceiling] if {$found>=0} { incr :windowHits } return $found } if {$position<${:bufferFrom} || $position>=${:bufferEnd} || ${:windowCeiling}!=$ceiling} { seek ${:channel} $position set expected [expr {min(${:readBytes},$ceiling-$position)}] set started [clock clicks -microseconds] set bytes [read ${:channel} $expected] incr :readMicroseconds [expr {max(0,[clock clicks -microseconds]-$started)}] incr :sourceBytes [string length $bytes] incr :readCalls if {[string length $bytes]<$expected} { error "source ended before its fixed scanner bound" } :setBuffer $bytes $position $ceiling } set found [:first $position [expr {min($ceiling,${:bufferEnd})}]] # A short batch key near the end cannot win before a longer key # beginning earlier but crossing this boundary has been checked. if {$found>=0 && (${:bufferEnd}>=$ceiling || $found<${:bufferEnd}-${:overlap})} { incr :windowHits return $found } if {${:bufferEnd}>=$ceiling} { return -1 } if {${:windowHits}==0} { set :readBytes [expr {min(${:blockBytes},2*${:readBytes})}] } else { set :readBytes [expr {min(${:blockBytes},65536)}] } set position [expr {max($from,${:bufferEnd}-${:overlap})}] # Refill even though the overlap is inside the previous buffer. set :windowCeiling -1 } return -1 } # In-memory physical-line candidates for access logs. A caller can request # sequential lines when normalization makes a raw literal prefilter unsafe. :public method nextLine {position {filter true}} { # # Read an in-memory physical line, optionally using the literal prefilter. # # Return physical byte bounds and newline status without parsing or # redacting the record. # # @param position Physical byte cursor within the loaded chunk. # @param filter Whether to apply the literal candidate prefilter; false # reads sequentially. Defaults to 'true'. # # @return A dictionary with start, end, raw and terminated fields, or an # empty result at exhaustion. # if {${:channel} ne ""} { error "nextLine requires an in-memory scanner" } if {$position<0 || $position>${:limit}} { error "invalid line cursor" } if {$position==${:limit}} { return {} } if {$filter} { set found [:find $position] if {$found<0} { return {} } set start [expr {[string last \n ${:buffer} [expr {$found-1}]]+1}] } else { set start $position } set end [string first \n ${:buffer} $start] set terminated [expr {$end>=0}] if {!$terminated} { set end ${:limit} } return [dict create start $start end [expr {$end+($terminated ? 1 : 0)}] raw [string range ${:buffer} $start [expr {$end-1}]] terminated $terminated] }XQL Not present: Generic, PostgreSQL, Oracle
![[i]](/resources/acs-subsite/ZoomIn16.gif)