nsmcp::CachedLogSearch method search (public)

 <instance of nsmcp::CachedLogSearch[i]> 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
[ hide source ] | [ make this the default ]
Show another procedure: