nsmcp::CachedLogSearch method search (public)
<instance of nsmcp::CachedLogSearch> search literal [ severity ] \ [ correlationField ] [ maxResults ]
Defined in /usr/local/ns/tcl/nsmcp/log-summary.tcl
Search raw records for literal text or exact request identifiers. Use one literal key for ordinary searches or a bounded identifier batch for the systemMany/accessMany correlation modes. Retain partial-scan and result-limit evidence.
- Parameters:
- literal (required)
- Literal search text; an empty value disables this additional text filter.
- severity (optional)
- System-log severity name to match.
- correlationField (optional)
- Exact header-field correlation mode, including systemMany/accessMany batches.
- maxResults (optional, defaults to
"0")- Retained record cap; zero keeps the legacy slice behavior. Defaults to '0'.
- Returns:
- The cached snapshot with explicit partial-scan and result-limit flags.
- Testcases:
- No testcase defined.
Source code: if {![string is integer -strict $maxResults] || $maxResults < 0 || $maxResults > 100} { error "result limit must be 0..100" } set many [expr {$correlationField in {systemMany accessMany}}] if {$many} { if {[llength $literal] < 1 || [llength $literal] > 20} { error "expected 1..20 request identifiers" } set literal [lsort -unique $literal] foreach id $literal { set check [expr {$correlationField eq "accessMany" ? "validConnectionIdentifier" : "validRequestIdentifier"}] if {[string length [encoding convertto utf-8 $id]] > 512 || ![${:recordParser} $check $id]} { error "correlation requires complete request identifiers" } } } if {$correlationField ni {{} system access systemMany accessMany}} { error "expected an empty, system or access correlation field" } set identifierCheck [expr {$correlationField eq "access" ? "validConnectionIdentifier" : "validRequestIdentifier"}] if {!$many && $correlationField ne "" && ![${:recordParser} $identifierCheck $literal]} { error "correlation requires a complete request identifier" } if {$severity ne "" && ![regexp {^[A-Za-z][A-Za-z0-9_()-]*$} $severity]} { error "expected a log severity name" } if {!$many && ($literal eq "" || [string length [encoding convertto utf-8 $literal]] > 4096 || [string first \n $literal] >= 0 || [string first \r $literal] >= 0 || [string first \x00 $literal] >= 0)} { error "expected a nonempty, single-line literal search key of at most 4096 bytes" } ns_mutex lock ${:mutex} try { set metaKey ${:cacheKey}:search set queryKey [ns_crypto::md string -digest sha256 -- [list log-metadata-v8 $literal $severity $correlationField $maxResults]] file stat ${:path} before set source [open ${:path} rb] set scanner [open ${:path} rb] set bounded {} try { fconfigure $source -translation binary -encoding binary fconfigure $scanner -translation binary -encoding binary file stat ${:path} opened set identity [list $before(dev) $before(ino)] if {$identity ne [list $opened(dev) $opened(ino)]} { error "log rotated while opening; retry" } set keys [expr {$many ? $literal : [list $literal]}] set bounded [::nsmcp::BoundedLogScanner new -channel $scanner -limit $before(size) -literals $keys -cacheDirectory ${:cacheDirectory}] set reset false set generation 1 if {[nsv_exists ${:store} $metaKey]} { set meta [nsv_get ${:store} $metaKey] set generation [dict get $meta generation] set prefix [dict get $meta prefix] if {$identity ne [dict get $meta identity] || $before(size) < [dict get $meta observedSize] || [read $source [string length $prefix]] ne $prefix} { :removeQueries [dict get $meta queries] set reset true incr generation } } else { set reset true } if {$reset} { seek $source 0 set meta [dict create identity $identity generation $generation observedSize $before(size) queries {} prefix [read $source [expr {min(256,$before(size))}]]] } if {[dict get $meta prefix] eq "" && $before(size) > 0} { seek $source 0 dict set meta prefix [read $source [expr {min(256,$before(size))}]] } set queries [dict get $meta queries] if {![dict exists $queries $queryKey]} { if {[dict size $queries] >= ${:maxSearches}} { set oldest {} set oldestTime {} dict for {key query} $queries { if {$oldest eq "" || [dict get $query used] < $oldestTime} { set oldest $key set oldestTime [dict get $query used] } } :removeQueries [dict create $oldest [dict get $queries $oldest]] dict unset queries $oldest } dict set queries $queryKey [dict create resultFile [:temporaryFile] pendingFile [:temporaryFile] resultHandle [ns_crypto::randombytes -encoding hex 32] cursor 0 resultCount 0 fileBytes 0 pending false used [clock clicks -milliseconds] cacheFull false limitReached false sourceBytes -1 scanIncomplete true] } set query [dict get $queries $queryKey] # Publish ownership before processing, so an interrupted scan's # files remain discoverable for invalidation/eviction. dict set meta queries $queries nsv_set ${:store} $metaKey $meta if {[dict get $query limitReached] || ([dict get $query sourceBytes] == $before(size) && ![dict get $query scanIncomplete])} { dict set query used [clock clicks -milliseconds] dict set meta queries $queryKey $query dict set meta observedSize $before(size) nsv_set ${:store} $metaKey $meta return [dict merge $query [dict create generation $generation reset $reset]] } set output [open [dict get $query resultFile] r+] set pending [open [dict get $query pendingFile] w] try { fconfigure $output -encoding utf-8 -translation lf fconfigure $pending -encoding utf-8 -translation lf # Discard writes beyond the last published checkpoint after # a failed scan, rather than appending duplicate results. ns_ftruncate $output [dict get $query fileBytes] seek $output 0 end dict set query pending false set cursor [dict get $query cursor] set matched 0 set byteBudgetReached false set scanEnd [expr {$many ? min($before(size),$cursor+8388608) : $before(size)}] while {$cursor < $scanEnd && $matched < ${:maxMatchesPerCall} && ![dict get $query cacheFull] && ($maxResults == 0 || [dict get $query resultCount] < $maxResults)} { # Shared bounded candidate search; record framing and exact # metadata checks below retain their format-specific behavior. set found [$bounded find $cursor $scanEnd] if {$found < 0 && $many && $scanEnd < $before(size)} { set overlap 0 foreach id $literal { set overlap [expr {max($overlap,[string length $id]-1)}] } set cursor [expr {$scanEnd-$overlap}] set byteBudgetReached true break } if {$found < 0 || $found >= $before(size)} { set cursor [:recordStart $source [expr {$before(size)-1}] $before(size)] break } set start [:recordStart $source $found $before(size)] set event [:record $source $start $before(size)] incr matched set metadata [${:recordParser} metadata [dict get $event message]] set accepted [expr {$severity eq "" || [dict get $metadata severity] eq $severity}] if {$correlationField eq "system"} { set accepted [expr {$accepted && [dict get $metadata requestIdentifier] eq $literal}] } elseif {$correlationField eq "access"} { set accepted [expr {$accepted && [${:recordParser} accessRequestIdentifier [dict get $event message]] eq $literal}] } elseif {$correlationField eq "systemMany"} { set accepted [expr {$accepted && [dict get $metadata requestIdentifier] in $literal}] } elseif {$correlationField eq "accessMany"} { set accepted [expr {$accepted && [${:recordParser} accessRequestIdentifier [dict get $event message]] in $literal}] } if {!$accepted} { if {![dict get $event complete]} { set cursor $start break } set cursor [dict get $event endOffset] continue } set json [:encodeEvent $event] if {![dict get $event complete]} { puts $pending $json dict set query pending true set cursor $start break } set bytes [string length [encoding convertto utf-8 $json\n]] if {[tell $output]+$bytes > ${:maxCacheBytes}} { dict set query cacheFull true set cursor $start break } puts $output $json dict incr query resultCount set cursor [dict get $event endOffset] } flush $output dict set query fileBytes [tell $output] dict set query cursor $cursor dict set query used [clock clicks -milliseconds] } finally { close $output close $pending } file stat ${:path} after seek $source 0 set prefix [dict get $meta prefix] if {$identity ne [list $after(dev) $after(ino)] || $after(size) < $before(size) || [read $source [string length $prefix]] ne $prefix} { error "log changed during scan; retry" } # cursor may point to the pending final message. Distinguish that # deliberate recheck from a match-budget backlog. if {$many && $cursor >= $scanEnd && $scanEnd < $before(size)} { set byteBudgetReached true } dict set query limitReached [expr {$maxResults > 0 && [dict get $query resultCount] >= $maxResults}] dict set query scanIncomplete [expr {$matched >= ${:maxMatchesPerCall} || [dict get $query cacheFull] || [dict get $query limitReached] || $byteBudgetReached}] dict set query sourceBytes $before(size) dict set meta observedSize $before(size) dict set meta queries $queryKey $query nsv_set ${:store} $metaKey $meta return [dict merge $query [dict create generation $generation reset $reset]] } finally { if {$bounded ne ""} { $bounded destroy } close $source close $scanner } } finally { ns_mutex unlock ${:mutex} }XQL Not present: Generic, PostgreSQL, Oracle
![[i]](/resources/acs-subsite/ZoomIn16.gif)