::nsmcp::CachedLogSearch ::nsmcp::CachedLogSearch encodeEvent errorSummary flush init lineStart physicalLine record recordStart removeQueries results search temporaryFile ::nsmcp::FileLogSource ::nsmcp::FileLogSource appendText consume emptyState init joinLine read ::nsmcp::CachedLogSearch->::nsmcp::FileLogSource ::nsmcp::LogSource ::nsmcp::LogSource read ::nsmcp::FileLogSource->::nsmcp::LogSource ::nsmcp::Object ::nsmcp::Object ::nsmcp::LogSource->::nsmcp::Object

Class ::nsmcp::CachedLogSearch

::nsmcp::CachedLogSearch[i] create ... \
           [ -cacheDirectory:required cacheDirectory:required ] \
           [ -cacheKey cacheKey ] \
           [ -maxCacheBytes:integer (default "67108864") ] \
           [ -maxEvents:integer (default "1000") ] \
           [ -maxMatchesPerCall:integer (default "1000") ] \
           [ -maxMessageBytes:integer (default "65536") ] \
           [ -maxReadBytes:integer (default "1048576") ] \
           [ -maxSearches:integer (default "16") ] \
           [ -mutex (default "") ] \
           [ -path:required path:required ] \
           [ -recordParser (default "::nsmcp::logRecordParser") ] \
           [ -store:required store:required ]

Defined in /usr/local/ns/tcl/nsmcp/log-summary.tcl

Class Relations

  • class: ::nx::Class[i]
  • superclass: ::nsmcp::FileLogSource[i]
::nx::Class create ::nsmcp::CachedLogSearch \
     -superclass ::nsmcp::FileLogSource

Methods (to be applied on instances)

  • errorSummary (scripted, public)

     <instance of nsmcp::CachedLogSearch[i]> errorSummary options \
        redactor

    Aggregate errors from the configured system-log source. Apply bounded scanning and redaction, retaining resumable state when a scan slice cannot finish.

    Parameters:
    options (required)
    Tcl dictionary containing the operation options.
    redactor (required)
    Redactor object applied before returning diagnostic evidence.
    Returns:
    The error-summary result with continuation state when needed.

    Testcases:
    No testcase defined.
    set from [dict get $options from]
    set to [dict get $options to]
    set maxGroups [dict get $options maxGroups]
    set literal [:dict getdef $options literal {}]
    set urlStats [::nsmcp::summaryUrlEmpty]
    set literalUncertain 0
    set literalUnknownTimeUncertain 0
    set severities [:dict getdef $options severities {Error Fatal Bug}]
    set selection [list $from $to $severities $literal]
    file stat ${:path} before
    set source [open ${:path} rb]
    set groups {}
    set matched 0
    set ungrouped 0
    set overflowSample {entries {} maximum 0}
    set overflowFamilySamples {}
    set sampleSeed {}
    set otherSources {}
    set otherUnknown 0
    set otherOmitted 0
    set familyTotals [::nsmcp::summaryFamilyCounts]
    set overflowFamilyTotals [::nsmcp::summaryFamilyCounts]
    set applicationMarkers 0
    set applicationSuppressed 0
    set categoryTotals {}
    set duplicateOrigins [::nsmcp::warningOriginEmpty]
    set parameterOrigins {}
    set omittedOriginParameterRecords 0
    set previousByThread {}
    set repeatMarkers 0
    set reportedSuppressed 0
    set attributedMarkers 0
    set unattributedMarkers 0
    set crossWindowMarkers 0
    set repeatExamples {}
    set unknownTime 0
    set unrecognized 0
    set examples {}
    set timestampedRecords 0
    set earliestAt {}
    set latestAt {}
    set firstEntryAt {}
    set lastEntryAt {}
    set firstEntryOffset {}
    set lastEntryOffset {}
    set cachedStamp {}
    set cachedSeconds {}
    set postgresRecords 0
    set postgresExamples {}
    set previousTimestamp {}
    set pendingPostgres {}
    set mutexTraces 0
    set mutexObservations 0
    set mutexUngroupedObservations 0
    set mutexUngroupedTotal 0.0
    set mutexGroups {}
    set mutexUngrouped 0
    set shortEntries 0
    set untimestamped 0
    set untimestampedExamples {}
    set severityTotals {}
    set unknownTimeTotals {}
    foreach severity $severities {
        dict set severityTotals $severity 0
        dict set unknownTimeTotals $severity 0
    }
    set pending false
    set clipped 0
    set firstTime {}
    set lastTime {}
    set deadline [expr {[clock milliseconds]+1000}]
    set start 0
    set examined 0
    set stable true
    set prefix {}
    if {[dict exists $options scanState]} {
        set state [dict get $options scanState]
        if {([dict exists $state selection] && [dict get $state selection] ne $selection)
            || ($literal ne {} && ![dict exists $state urlStats])} {
            close $source
            error "system-log literal summary changed during scan; start a new summary"
        }
        foreach name {urlStats literalUncertain literalUnknownTimeUncertain groups matched ungrouped overflowSample overflowFamilySamples sampleSeed otherSources otherUnknown otherOmitted familyTotals overflowFamilyTotals applicationMarkers applicationSuppressed categoryTotals duplicateOrigins parameterOrigins omittedOriginParameterRecords previousByThread repeatMarkers reportedSuppressed attributedMarkers unattributedMarkers crossWindowMarkers repeatExamples unknownTime unrecognized examples severityTotals unknownTimeTotals pending clipped firstTime lastTime start examined prefix mutexTraces mutexObservations mutexUngroupedObservations mutexUngroupedTotal mutexGroups mutexUngrouped shortEntries untimestamped untimestampedExamples postgresRecords postgresExamples previousTimestamp pendingPostgres timestampedRecords earliestAt latestAt firstEntryAt lastEntryAt firstEntryOffset lastEntryOffset cachedStamp cachedSeconds} {
            if {[dict exists $state $name]} {
                set $name [dict get $state $name]
            }
        }
        if {$matched > 0 && (![dict exists $state familyTotals] || ![dict exists $state otherSources])} {
            close $source
            error "system-log family summary changed during scan; start a new summary"
        }
        if {![dict exists $state mutexObservations] && ($examined>0 || $mutexTraces>0)} {
            close $source
            error "system-log mutex accounting changed during scan; start a new summary"
        }
        if {![dict exists $state mutexGroups]} {
            set mutexUngrouped $mutexTraces
        }
        if {[list $before(dev) $before(ino)] ne [dict get $state identity]
            || $before(size) < [dict get $state sourceBytes]} {
            close $source
            error "system log changed during summary scan; start a new summary"
        }
        set before(size) [dict get $state sourceBytes]
    }
    if {$sampleSeed eq ""} {
        set sampleSeed [list $before(dev) $before(ino) $before(size) $from $to $severities]
    }
    set chunkEnd [expr {min($before(size),$examined+4194304)}]
    set reader {}
    try {
        set currentPrefix [read $source [expr {min(256,$before(size))}]]
        if {$prefix ne "" && $prefix ne $currentPrefix} {
            error "system log changed during summary scan; start a new summary"
        }
        set prefix $currentPrefix
        seek $source $examined
        set reader [::nsmcp::LogSummaryReader new -channel $source -limit $before(size) -offset $examined -maxBytes ${:maxMessageBytes}]
        while {$examined < $chunkEnd && [clock milliseconds] < $deadline} {
            set offset $examined
            set event [$reader next]
            set examined [dict get $event endOffset]
            if {$examined <= $offset} {
                break
            }
            if {![dict get $event complete]} {
                set pending true
            }
            # Neighboring timestamp strings are contextual estimates, never dates
            # assigned to the diagnostic or used for time-window error totals.
            if {[regexp {^\[([0-9]{2}/[A-Za-z]{3}/[0-9]{4}:[^\]\r\n]+)\]\[[^\]]+\]\[[^\]]+\]} [dict get $event message] -> stamp]} {
                if {$stamp ne $cachedStamp} {
                    set cachedStamp $stamp
                    set cachedSeconds [${:recordParser} timestampSeconds $stamp]
                }
                if {$cachedSeconds ne ""} {
                    incr timestampedRecords
                    if {$firstEntryAt eq ""} {
                        set firstEntryAt $cachedSeconds
                        set firstEntryOffset $offset
                    }
                    set lastEntryAt $cachedSeconds
                    set lastEntryOffset $offset
                    if {$earliestAt eq "" || $cachedSeconds<$earliestAt} {
                        set earliestAt $cachedSeconds
                    }
                    if {$latestAt eq "" || $cachedSeconds>$latestAt} {
                        set latestAt $cachedSeconds
                    }
                }
                foreach i $pendingPostgres {
                    set fields [lindex $postgresExamples $i]
                    lappend fields followingTimestamp string $stamp
                    lset postgresExamples $i $fields
                }
                set pendingPostgres {}
                set previousTimestamp $stamp
            }
            # Most records are Notice/Debug output. Avoid parser dispatch and
            # metadata allocation for valid headers outside the counted levels.
            if {[regexp {^\[[^\]\r\n]+\]\[[^\]\r\n]+\]\[[^\]\r\n]+\][ \t]+([A-Za-z][A-Za-z0-9_()-]*):[ \t]*} [dict get $event message] -> level]
                && $level ni $severities} {
                continue
            }
            set meta [${:recordParser} metadata [dict get $event message]]
            set recordType [dict get $meta recordType]
            if {$recordType eq "postgres"} {
                incr postgresRecords
                if {[llength $postgresExamples]<5} {
                    set text [$redactor message [dict get $event message]]
                    lappend pendingPostgres [llength $postgresExamples]
                    lappend postgresExamples [list offset number $offset endOffset number $examined  severity string [dict get $meta severity] example string [string range $text 0 511]  truncated boolean [expr {[dict get $event truncated] || [string length $text]>512}]  timeSource string inferred-neighbors precedingTimestamp string $previousTimestamp]
                }
            }
            if {$recordType eq "mutex-trace"} {
                # Scheduler condition waits are expected implementation details,
                # not contention evidence. Exclude only this exact hold trace;
                # ns:sched lock waits and negative-wait warnings remain visible.
                if {[dict get $meta mutexName] eq "ns:sched"
                    && [dict get $meta mutexOperation] eq "unlock"
                    && [dict get $meta mutexMetric] eq "hold"} {
                    continue
                }
                incr mutexTraces
                set mutexKey [list [dict get $meta mutexName] [dict get $meta mutexOperation] [dict get $meta mutexMetric]]
                set duration [dict get $meta mutexDurationSeconds]
                set deferred [expr {[dict exists $meta mutexDeferred] && [dict get $meta mutexDeferred]}]
                set observations [expr {$deferred ? [dict get $meta mutexObservationCount] : 1}]
                set total [expr {$deferred ? [dict get $meta mutexTotalSeconds] : $duration}]
                incr mutexObservations $observations
                if {[dict exists $mutexGroups $mutexKey]} {
                    set group [dict get $mutexGroups $mutexKey]
                    dict incr group count
                    dict incr group observations $observations
                    if {$deferred} {
                        dict incr group deferredRecords
                        dict incr group deferredObservations $observations
                    }
                    if {$deferred && $observations>1} {
                        dict set group minimumKnown false
                    }
                    dict set group total [expr {[dict get $group total]+$total}]
                    dict set group min [expr {min([dict get $group min],$duration)}]
                    dict set group lastOffset $offset
                    if {$duration > [dict get $group max]} {
                        dict set group max $duration
                        dict set group maxOffset $offset
                        dict set group maxThread [$redactor message [dict get $meta logThread]]
                    }
                    if {$duration<0} {
                        dict incr group negative
                    }
                    dict set mutexGroups $mutexKey $group
                } elseif {[dict size $mutexGroups]<128} {
                    dict set mutexGroups $mutexKey [dict create count 1 observations $observations total $total min $duration max $duration  deferredRecords [expr {$deferred ? 1 : 0}] deferredObservations [expr {$deferred ? $observations : 0}]  minimumKnown [expr {!$deferred || $observations==1}]  firstOffset $offset lastOffset $offset maxOffset $offset  maxThread [$redactor message [dict get $meta logThread]] negative [expr {$duration<0}]]
                } else {
                    incr mutexUngrouped
                    incr mutexUngroupedObservations $observations
                    set mutexUngroupedTotal [expr {$mutexUngroupedTotal+$total}]
                }
            }
            if {$recordType eq "system-short"} {
                incr shortEntries
            }
            if {$recordType in {mutex-trace system-short postgres}} {
                incr untimestamped
                if {[llength $untimestampedExamples]<5} {
                    set text [$redactor message [dict get $event message]]
                    set fields [list offset number $offset endOffset number $examined  recordType string $recordType logThread string [dict get $meta logThread]  severity string [dict get $meta severity] example string [string range $text 0 511]  truncated boolean [expr {[dict get $event truncated] || [string length $text]>512}]]
                    foreach key {mutexName mutexOperation mutexMetric} {
                        if {[dict exists $meta $key]} {
                            lappend fields $key string [$redactor message [dict get $meta $key]]
                        }
                    }
                    foreach key {mutexDurationSeconds mutexObservationCount mutexTotalSeconds} {
                        if {[dict exists $meta $key]} {
                            lappend fields $key number [dict get $meta $key]
                        }
                    }
                    if {[dict exists $meta mutexDeferred]} {
                        lappend fields mutexDeferred boolean [dict get $meta mutexDeferred]
                    }
                    lappend untimestampedExamples $fields
                }
            }
            if {$recordType eq "mutex-trace"} {
                continue
            }
            set severity [dict get $meta severity]
            if {$severity eq ""} {
                incr unrecognized
                if {[llength $examples] < 5} {
                    lappend examples [list offset number $offset endOffset number $examined  example string [string range [$redactor message [dict get $event message]] 0 511]  truncated boolean [expr {[dict get $event truncated] || [string length [dict get $event message]]>512}]]
                }
                continue
            }
            if {$severity ni $severities} {
                continue
            }
            set literalMatched [expr {$literal eq {} || [string first $literal [dict get $event message]]>=0}]
            set timestamp [dict get $meta timestamp]
            if {$timestamp eq $cachedStamp} {
                set seconds $cachedSeconds
            } else {
                set seconds [${:recordParser} timestampSeconds $timestamp]
            }
            if {$seconds eq "" || ![string is double -strict $seconds]} {
                if {!$literalMatched} {
                    if {[dict get $event truncated]} {
                        incr literalUnknownTimeUncertain
                    }
                    continue
                }
                if {$severity in {Error Fatal Bug}} {
                    incr unknownTime
                }
                dict incr unknownTimeTotals $severity
                continue
            }
            set text [$redactor message [dict get $event message]]
            set normalized [::nsmcp::summaryHeading $text]
            set category [dict get $normalized category]
            set threadKey {}
            regexp {^\[[^\]]+\]\[([^\]]+)\]} [dict get $event message] -> threadKey
            set prior {}
            if {$threadKey ne "" && [dict exists $previousByThread $threadKey]} {
                set prior [dict get $previousByThread $threadKey]
            }
            if {$category ne "log-repeat-marker" && $threadKey ne ""
                && ([dict exists $previousByThread $threadKey] || [dict size $previousByThread] < 2048)} {
                dict set previousByThread $threadKey [dict create timestamp $seconds severity $severity offset $offset pattern [dict get $normalized pattern]]
            }
            if {$seconds < $from || $seconds >= $to} {
                continue
            }
            if {!$literalMatched} {
                if {[dict get $event truncated]} {
                    incr literalUncertain
                }
                continue
            }
            incr matched
            if {$literal ne {}} {
                ::nsmcp::summaryUrlAdd urlStats [dict get $event message] $seconds [dict get $meta requestIdentifier] $redactor [dict get $event truncated]
            }
            dict incr severityTotals $severity
            if {$firstTime eq "" || $seconds < $firstTime} {
                set firstTime $seconds
            }
            if {$lastTime eq "" || $seconds > $lastTime} {
                set lastTime $seconds
            }
            dict incr categoryTotals $category
            set family [::nsmcp::summaryFamily $normalized]
            dict incr familyTotals $family
            if {$family eq "other"} {
                # Require an emitter-style identifier followed by a colon;
                # free-form headings remain explicitly unattributed.
                set heading [dict get $normalized pattern]
                if {[regexp {^([A-Za-z_][A-Za-z0-9_:.-]{0,79}):[ \t]} $heading -> emitter]} {
                    if {[dict exists $otherSources $emitter] || [dict size $otherSources] < 32} {
                        dict incr otherSources $emitter
                    } else {
                        incr otherOmitted
                    }
                } else {
                    incr otherUnknown
                }
            }
            if {[dict get $normalized suppressedOccurrences] > 0} {
                incr applicationMarkers
                incr applicationSuppressed [dict get $normalized suppressedOccurrences]
            }
            if {$category eq "log-repeat-marker"} {
                incr repeatMarkers
                # Native sameLineCount includes the originally written message.
                scan [dict get $normalized repeats] %lld runCount
                set suppressed [expr {max(0,$runCount-1)}]
                incr reportedSuppressed $suppressed
                set attributed [expr {$prior ne "" && [dict get $prior severity] eq $severity}]
                if {$attributed} {
                    incr attributedMarkers
                    if {[dict get $prior timestamp] < $from || [dict get $prior timestamp] >= $to} {
                        incr crossWindowMarkers
                    }
                } else {
                    incr unattributedMarkers
                }
                if {[llength $repeatExamples] < 5} {
                    set fields [list markerOffset number $offset markerTimestamp number $seconds reportedSuppressedOccurrences number $suppressed attributed boolean $attributed]
                    if {$attributed} {
                        lappend fields precedingOffset number [dict get $prior offset] precedingTimestamp number [dict get $prior timestamp] precedingPattern string [dict get $prior pattern]
                    }
                    lappend repeatExamples $fields
                }
            }
            if {$category eq "duplicate-parameter"} {
                set requestMetadata [::nsmcp::warningRequestMetadata [dict get $event message] $redactor]
                ::nsmcp::warningOriginAdd duplicateOrigins $requestMetadata 128
                set parameter [dict get $normalized parameter]
                if {[dict exists $parameterOrigins $parameter] || [dict size $parameterOrigins] < 128} {
                    set origins [expr {[dict exists $parameterOrigins $parameter] ? [dict get $parameterOrigins $parameter] : [::nsmcp::warningOriginEmpty]}]
                    ::nsmcp::warningOriginAdd origins $requestMetadata 32
                    dict set parameterOrigins $parameter $origins
                } else {
                    incr omittedOriginParameterRecords
                }
            }
            set pattern [dict get $normalized pattern]
            set key [list $severity $pattern]
            if {![dict exists $groups $key]} {
                if {[dict size $groups] >= 512} {
                    incr ungrouped
                    dict incr overflowFamilyTotals $family
                    set priority [zlib crc32 [list $sampleSeed $offset]]
                    set sample [:dict getdef $overflowFamilySamples $family {entries {} maximum 0}]
                    if {[llength [dict get $overflowSample entries]] < 5 || $priority < [dict get $overflowSample maximum]
                        || [llength [dict get $sample entries]] < 3 || $priority < [dict get $sample maximum]} {
                        set fields [list offset number $offset endOffset number $examined  timestamp number $seconds severity string $severity category string $category family string $family  pattern string $pattern example string [string range $text 0 511]  truncated boolean [expr {[dict get $event truncated] || [string length $text] > 512}]]
                        ::nsmcp::summaryOverflowSample overflowSample 5 $priority $offset $fields
                        ::nsmcp::summaryOverflowSample sample 3 $priority $offset $fields
                        dict set overflowFamilySamples $family $sample
                    }
                    continue
                }
                dict set groups $key [dict create severity $severity pattern $pattern category $category parameter {} samples {} parameterCounts {} omittedParameterRecords 0 schedulerStats {} suppressedSuffixRecords 0 reportedSuppressedOccurrences 0 count 0  firstAt $seconds lastAt $seconds example [string range $text 0 2047]  requestIdentifier [dict get $meta requestIdentifier] exampleTruncated false]
            }
            set group [dict get $groups $key]
            dict incr group count
            if {[dict get $normalized suppressedOccurrences] > 0} {
                dict incr group suppressedSuffixRecords
                dict incr group reportedSuppressedOccurrences [dict get $normalized suppressedOccurrences]
            }
            if {$category eq "duplicate-parameter"} {
                set parameter [dict get $normalized parameter]
                set counts [dict get $group parameterCounts]
                if {[dict exists $counts $parameter] || [dict size $counts] < 128} {
                    dict incr counts $parameter
                    dict set group parameterCounts $counts
                } else {
                    dict incr group omittedParameterRecords
                }
            }
            if {$category eq "scheduler-lateness"} {
                dict set group scheduler [dict get $normalized scheduler]
                dict for {name value} [dict get $normalized timings] {
                    if {[dict exists $group schedulerStats $name]} {
                        set metric [dict get $group schedulerStats $name]
                        dict incr metric count
                        dict set metric total [expr {[dict get $metric total] + $value}]
                        dict set metric min [expr {min([dict get $metric min],$value)}]
                        dict set metric max [expr {max([dict get $metric max],$value)}]
                    } else {
                        set metric [dict create count 1 total $value min $value max $value]
                    }
                    dict set group schedulerStats $name $metric
                }
            }
            if {$category eq "duplicate-parameter" && [llength [dict get $group samples]] < 12
                && [::nsmcp::logRecordParser validConnectionIdentifier [dict get $meta requestIdentifier]]} {
                set retainedForParameter 0
                foreach retained [dict get $group samples] {
                    if {[dict exists $retained parameter] && [dict get $retained parameter] eq $parameter} {
                        incr retainedForParameter
                    }
                }
                set sample [dict merge [dict create parameter $parameter requestIdentifier [dict get $meta requestIdentifier] timestamp $seconds] $requestMetadata]
                if {$retainedForParameter < 3 && $sample ni [dict get $group samples]} {
                    dict lappend group samples $sample
                }
            }
            dict set group firstAt [expr {min([dict get $group firstAt],$seconds)}]
            dict set group lastAt [expr {max([dict get $group lastAt],$seconds)}]
            if {[dict get $event truncated] || [string length $text]>2048} {
                dict set group exampleTruncated true
                incr clipped
            }
            dict set groups $key $group
        }
        file stat ${:path} after
        seek $source 0
        set stable [expr {[list $before(dev) $before(ino)] eq [list $after(dev) $after(ino)]  && $after(size)>=$before(size) && [read $source [string length $prefix]] eq $prefix}]
    } finally {
        if {$reader ne ""} {
            $reader destroy
        }
        close $source
    }
    if {!$stable} {
        error "system log changed during summary scan; start a new summary"
    }
    set state {}
    if {$examined < $before(size)} {
        foreach name {urlStats literalUncertain literalUnknownTimeUncertain groups matched ungrouped overflowSample overflowFamilySamples sampleSeed otherSources otherUnknown otherOmitted familyTotals overflowFamilyTotals applicationMarkers applicationSuppressed categoryTotals duplicateOrigins parameterOrigins omittedOriginParameterRecords previousByThread repeatMarkers reportedSuppressed attributedMarkers unattributedMarkers crossWindowMarkers repeatExamples unknownTime unrecognized examples severityTotals unknownTimeTotals pending clipped firstTime lastTime start examined prefix mutexTraces mutexObservations mutexUngroupedObservations mutexUngroupedTotal mutexGroups mutexUngrouped shortEntries untimestamped untimestampedExamples postgresRecords postgresExamples previousTimestamp pendingPostgres timestampedRecords earliestAt latestAt firstEntryAt lastEntryAt firstEntryOffset lastEntryOffset cachedStamp cachedSeconds} {
            dict set state $name [set $name]
        }
        dict set state selection $selection
        dict set state identity [list $before(dev) $before(ino)]
        dict set state sourceBytes $before(size)
    }
    set exampleEntries {}
    set i 0
    foreach example $examples {
        lappend exampleEntries $i object $example
        incr i
    }
    set untimestampedEntries {}
    set i 0
    foreach example $untimestampedExamples {
        lappend untimestampedEntries $i object $example
        incr i
    }
    set mutexEntries {}
    set i 0
    dict for {key group} $mutexGroups {
        lassign $key mutex operation metric
        lappend mutexEntries $i object [list mutexName string [$redactor message $mutex]  operation string $operation metric string $metric count number [dict get $group count]  recordCount number [dict get $group count] observationCount number [dict get $group observations]  deferredRecords number [dict get $group deferredRecords] deferredObservations number [dict get $group deferredObservations]  totalSeconds number [dict get $group total] minimumKnown boolean [dict get $group minimumKnown]  minSeconds [expr {[dict get $group minimumKnown] ? "number" : "null"}] [expr {[dict get $group minimumKnown] ? [dict get $group min] : {}}] maxSeconds number [dict get $group max]  meanSeconds number [expr {[dict get $group total]/double([dict get $group observations])}]  negativeDurations number [dict get $group negative]  firstOffset number [dict get $group firstOffset] lastOffset number [dict get $group lastOffset]  maxOffset number [dict get $group maxOffset] maxLogThread string [dict get $group maxThread]]
        incr i
    }
    set postgresEntries {}
    set i 0
    foreach example $postgresExamples {
        if {[lsearch -exact $example followingTimestamp]<0} {
            lappend example followingTimestamp string {}
        }
        lappend postgresEntries $i object $example
        incr i
    }
    set severityFields {}
    dict for {severity count} $severityTotals {
        lappend severityFields $severity number $count
    }
    set selectedFields {}
    set i 0
    foreach severity $severities {
        lappend selectedFields $i string $severity
        incr i
    }
    set unknownFields {}
    set unknownSelected 0
    dict for {severity count} $unknownTimeTotals {
        lappend unknownFields $severity number $count
        incr unknownSelected $count
    }
    set ordered {}
    dict for {key group} $groups {
        lappend ordered [list [dict get $group count] $group]
    }
    set entries {}
    set index 0
    set omittedGroups 0
    set omittedGroupRecords 0
    set returnedGroupRecords 0
    foreach pair [lsort -integer -decreasing -index 0 $ordered] {
        set group [lindex $pair 1]
        if {$index >= $maxGroups} {
            incr omittedGroups
            incr omittedGroupRecords [dict get $group count]
            continue
        }
        set fields {}
        foreach name {severity pattern category parameter example requestIdentifier} {
            lappend fields $name string [dict get $group $name]
        }
        foreach name {count firstAt lastAt suppressedSuffixRecords reportedSuppressedOccurrences} {
            lappend fields $name number [dict get $group $name]
        }
        if {[dict get $group category] eq "duplicate-parameter"} {
            set parameters {}
            dict for {parameter count} [dict get $group parameterCounts] {
                lappend parameters $parameter number $count
            }
            lappend fields parameterCounts object $parameters parameterCapacity number 128  omittedParameterRecords number [dict get $group omittedParameterRecords]  parameterBreakdownComplete boolean [expr {[dict get $group omittedParameterRecords] == 0}]
        }
        if {[dict get $group category] eq "scheduler-lateness"} {
            set scheduler [dict get $group scheduler]
            set details [list jobId number [dict get $scheduler jobId] flags string [dict get $scheduler flags]  intervalSeconds number [dict get $scheduler intervalSeconds]]
            set timings {}
            dict for {name metric} [dict get $group schedulerStats] {
                lappend timings $name object [list count number [dict get $metric count]  minSeconds number [dict get $metric min] maxSeconds number [dict get $metric max]  meanSeconds number [expr {[dict get $metric total]/double([dict get $metric count])}]]
            }
            lappend details timings object $timings
            lappend fields scheduler object $details
        }
        set samples {}
        set sampleIndex 0
        foreach sample [dict get $group samples] {
            set sampleFields [list requestIdentifier string [dict get $sample requestIdentifier] timestamp number [dict get $sample timestamp]]
            if {[dict exists $sample parameter]} {
                lappend sampleFields parameter string [dict get $sample parameter]
            }
            foreach name {authenticationState authenticationSource} {
                if {[dict exists $sample $name]} {
                    lappend sampleFields $name string [dict get $sample $name]
                }
            }
            if {[dict exists $sample userAgent]} {
                lappend sampleFields userAgent string [dict get $sample userAgent] userAgentSource string system-log  userAgentMayBeTruncated boolean [dict get $sample userAgentMayBeTruncated]
            }
            lappend samples $sampleIndex object $sampleFields
            incr sampleIndex
        }
        lappend fields requestSamples array $samples exampleTruncated boolean [dict get $group exampleTruncated]
        incr returnedGroupRecords [dict get $group count]
        lappend entries $index object $fields
        incr index
    }
    # Observed entry times describe only bytes scanned so far, not rotation or
    # proof of complete coverage. File mtime is deliberately not used as a date.
    set bounds [list source string observed-timestamped-entries scope string scanned-file  timestampedRecords number $timestampedRecords]
    foreach name {earliestAt latestAt firstEntryAt lastEntryAt firstEntryOffset lastEntryOffset} {
        set value [set $name]
        lappend bounds $name [expr {$value eq "" ? "null" : "number"}] $value
    }
    set logTimezone :localtime
    if {[info exists ::env(TZ)] && $::env(TZ) ne ""} {
        set logTimezone $::env(TZ)
    }
    # System-log rotations may be scheduled by application code or external
    # tools. nslog's logrollhour describes access logs, not this source.
    set rotation [list scheduleKnown boolean false schedule null {} lastVerifiedAt null {}  timezone string $logTimezone  onSignal boolean [ns_config -bool ns/parameters logrollonsignal  [ns_config -bool ns/parameters logroll true]]  filenameFormat string [ns_config ns/parameters logrollfmt {}]  configurationScope string process-system-log  limitations string {Signal handling and filename format are configuration, not proof of rotation. A daily schedule and last successful rotation time are not exposed by this interface; application or external rotation is possible.}]
    set categories {}
    dict for {category count} $categoryTotals {
        lappend categories $category number $count
    }
    set repeatFields {}
    set repeatIndex 0
    foreach example $repeatExamples {
        lappend repeatFields $repeatIndex object $example
        incr repeatIndex
    }
    set familyFields {}
    set overflowFamilyFields {}
    set familySamples {}
    set familyIndex 0
    dict for {family count} $familyTotals {
        lappend familyFields $family number $count
    }
    dict for {family count} $overflowFamilyTotals {
        lappend overflowFamilyFields $family number $count
        if {$count == 0} {
            continue
        }
        set examples [::nsmcp::summaryOverflowExamples [dict get $overflowFamilySamples $family]]
        set rows {}
        set n 0
        foreach example $examples {
            lappend rows $n object $example
            incr n
        }
        lappend familySamples $familyIndex object [list family string $family count number $count  exampleLimit number 3 examples array $rows omittedExampleRecords number [expr {$count-[llength $examples]}]]
        incr familyIndex
    }
    set overflowExamples [::nsmcp::summaryOverflowExamples $overflowSample]
    set overflowFields {}
    set overflowIndex 0
    foreach example $overflowExamples {
        lappend overflowFields $overflowIndex object $example
        incr overflowIndex
    }
    set dedup [list applicationMarkerRecords number $applicationMarkers applicationReportedSuppressedOccurrences number $applicationSuppressed configuredNow boolean [ns_config -bool ns/parameters logdeduplicate false]  markerRecords number $repeatMarkers messageRecords number [expr {$matched-$repeatMarkers}]  reportedSuppressedOccurrences number $reportedSuppressed attributedMarkers number $attributedMarkers  unattributedMarkers number $unattributedMarkers crossWindowMarkers number $crossWindowMarkers  examples array $repeatFields limitations string {Severity totals count physical records including repeat markers, not expanded occurrences. Markers inherit the preceding message severity. Native sameLineCount includes the originally written message; reported suppressed occurrences subtract one per marker. A marker timestamp does not locate suppressed occurrences in time or identify their requests. Attribution uses a bounded per-process/thread map over selected severities; rotation, map limits and missing prior entries prevent attribution. Current configuration is not historical configuration. Recognized trailing application/core suppression suffixes are counted separately in applicationMarkerRecords and applicationReportedSuppressedOccurrences, never added to severity totals or native reportedSuppressedOccurrences. Their suppressed event times and severity history are not established.}]
    set parameterFields {}
    set parameterIndex 0
    dict for {parameter origins} $parameterOrigins {
        lappend parameterFields $parameterIndex object [concat [list parameter string $parameter] [::nsmcp::warningOriginFields $origins 32]]
        incr parameterIndex
    }
    set originFields [list scope string selected-window-duplicate-records  overall object [::nsmcp::warningOriginFields $duplicateOrigins 128] parameters array $parameterFields  parameterCapacity number 128 omittedParameterRecords number $omittedOriginParameterRecords  parameterBreakdownComplete boolean [expr {$omittedOriginParameterRecords == 0}]  limitations string {Aggregates include all selected timestamped duplicate-parameter records in the scanned window before group/evidence limits, including grouping overflow. Counts are warning records, not distinct requests or users. Authentication is derived from the explicit OpenACS user_id diagnostic before redaction (0 anonymous, positive decimal authenticated, missing/invalid unknown); actual user IDs are never retained. User-Agents are decoded and redacted before grouping and may merge after redaction or truncation. Missing, empty or invalid User-Agents are unknown. Cardinality-overflow records, retained ranking omissions and truncated prefixes are separate. userAgentAggregationComplete requires no unknown, truncated or cardinality-overflow records, independently of output limits. userAgentRankingComplete additionally requires no withheld ranked records. userAgentCountsComplete is the compatibility alias for ranking completeness. Ranking omissions do not discard aggregated records. Rankings are not global rankings when omittedUserAgentRecords is nonzero. Self-reported User-Agents and pool membership do not verify bot/human identity. No access cache or peer addresses are required; file/window coverage limitations still apply.}]
    set otherFields {}
    dict for {emitter count} $otherSources {
        lappend otherFields $emitter number $count
    }
    set otherBreakdown [list recordCount number [dict get $familyTotals other]  sourceCounts object $otherFields sourceCapacity number 32  unknownSourceRecords number $otherUnknown omittedSourceRecords number $otherOmitted  sourceCountsComplete boolean [expr {$otherUnknown == 0 && $otherOmitted == 0}]  limitations string {Counts cover all selected other-family records before detailed grouping limits. Sources are bounded emitter-style heading prefixes, not verified subsystems or causes. Unknown source records lack a recognized prefix; omitted source records exceed the 32-source cardinality bound. Retained counts plus unknown and omitted equal recordCount. All retained source counts are returned; source text excludes message payloads.}]
    set urlFields [::nsmcp::summaryUrlFields $urlStats $maxGroups [expr {$examined==$before(size)}] $literalUncertain $redactor]
    lappend urlFields enabled boolean [expr {$literal ne {}}]
    set literalFields [list enabled boolean [expr {$literal ne {}}]  caseSensitive boolean true unknownMatchWindowRecords number $literalUncertain  unknownMatchUnknownTimeRecords number $literalUnknownTimeUncertain  complete boolean [expr {$examined==$before(size) && $literalUncertain==0}]  scope string {Literal matching uses the retained logical record, including continuation lines, after ANSI removal and before redaction. A literal absent from a clipped record is not a confirmed nonmatch; unknownMatchWindowRecords reports those records separately. This is not a unique-request count or a retention guarantee.}]
    set result [list {} object [list server string [ns_info server] scope string process  literalSelection object $literalFields urlAggregation object $urlFields  from number $from to number $to timezone string [dict get $options timezone]  timestampTimezone string [clock format [clock seconds] -format %Z]  selectedSeverities array $selectedFields severityTotals object $severityFields matchedRecords number $matched ungroupedRecords number $ungrouped  groupingComplete boolean [expr {$ungrouped == 0}] groupCapacity number 512  familyTotals object $familyFields otherBreakdown object $otherBreakdown overflowFamilyTotals object $overflowFamilyFields  overflowSamplesByFamily array $familySamples overflowFamilyExampleLimit number 3  overflowSamplingMethod string deterministic-crc32-priority  overflowExamples array $overflowFields overflowExampleLimit number 5  overflowExamplesOmittedRecords number [expr {max(0,$ungrouped-[llength $overflowExamples])}]  returnedGroupCount number $index returnedGroupRecords number $returnedGroupRecords  omittedGroups number $omittedGroups omittedGroupRecords number $omittedGroupRecords  categoryTotals object $categories duplicateParameterOrigins object $originFields deduplication object $dedup groups array $entries  grouping string {Known API procedure, missing-resource fallback, host-validation and HTTP request-buffer templates normalize variable names, paths and payloads while preserving redacted examples. otherBreakdown counts bounded emitter prefixes across all other-family records, including overflow, with unknown and cardinality-omitted counts. Known duplicate-parameter warnings share one group per severity with up to 128 parameterCounts and explicit omittedParameterRecords. Scheduler lateness groups retain job ID, flags and interval; varying durations are aggregated separately. Recognized suppression suffixes do not fragment keys. familyTotals classify every selected record into a fixed broad message family (other is explicit), independently of detailed-group capacity. overflowFamilyTotals are exact counts of excess records by family. These are physical record classifications, not verified causes or incidents. overflowExamples retain up to five redacted hash-priority samples across the examined overflow stream; overflowSamplesByFamily retains up to three per fixed family, with explicit omittedExampleRecords. Sampling uses deterministic CRC-32 priorities from fixed snapshot identity, bounds and byte offsets, retains state across scan continuation, and returns samples in byte order. It is not cryptographic or a guaranteed statistically unbiased sample; per-family examples are stratified, not proportional frequency evidence. Use exact family counts for distribution claims. Missing examples do not mean absent records. Up to 512 distinct groups are counted independently of maxGroups; the largest counted groups are returned. omittedGroupRecords are classified but not returned; ungroupedRecords exceeded internal capacity. Rankings are globally complete only when groupingComplete=true. Unknown message keys use redacted first lines with numeric tokens and hex addresses normalized.}  coverage object [list logBounds object $bounds rotation object $rotation sourceBytes number $before(size) examinedFromOffset number $start examinedToOffset number $examined  sourceFullyExamined boolean [expr {$stable && $start==0 && $examined==$before(size)}]  sourceChanged boolean [expr {!$stable}] historyUnreadBytes number $start  unreadBytes number [expr {max(0,$before(size)-$examined)}]  unknownTimeErrors number $unknownTime unknownTimeRecords number $unknownSelected  unknownTimeSeverityTotals object $unknownFields unrecognizedRecords number $unrecognized  unrecognizedExamples array $exampleEntries  mutexTraceRecords number $mutexTraces mutexTraceObservations number $mutexObservations mutexTraceGroups array $mutexEntries  mutexTraceUngroupedObservations number $mutexUngroupedObservations mutexTraceUngroupedTotalSeconds number $mutexUngroupedTotal  mutexTraceUngroupedRecords number $mutexUngrouped shortLogRecords number $shortEntries  untimestampedRecords number $untimestamped untimestampedExamples array $untimestampedEntries  postgresRecords number $postgresRecords postgresExamples array $postgresEntries  diagnosticScope string scanned-file  clippedExamples number $clipped pendingFinalRecord boolean $pending  rotationsExamined boolean false windowComplete boolean false  limitations string {Only the configured current system log is examined. Rotated files and time before this file are not certified. Counts include observed final records, whose continuations may still grow. Expected ns:sched unlock/hold diagnostics are omitted entirely. Mutex count and recordCount count physical diagnostic records; observationCount counts represented intervals. Deferred condition-wait summaries contribute their reported N and total T, never N times their maximum. meanSeconds is totalSeconds/observationCount. Minimum is null when a multi-interval summary prevents establishing it; maximum/thread attribution remain known. These threshold-selected observations are not lifetime ns_info locks statistics, and different scan/snapshot times or trace-enable periods prevent direct reconciliation. Other mutex traces are grouped by mutex name, operation and wait/hold metric over every scanned trace (up to 128 groups; excess records are counted in mutexTraceUngroupedRecords). Durations are individual observations, not additive wall time or proof of contention. Negative durations indicate timing diagnostics, not valid waits. Mutex traces and short system-log entries have no timestamps; diagnostic counters and examples cover the scanned file, not necessarily the requested time window. PostgreSQL DETAIL, HINT and CONTEXT fields are grouped with their diagnostic. postgresExamples provides explicitly inferred neighboring timestamp bounds, which may be missing or out of order under concurrent logging; these are contextual estimates, not guaranteed event times or request attribution. No timestamps are inherited from adjacent entries. Untimestamped selected severities contribute to unknownTimeRecords and unknownTimeSeverityTotals, not window severityTotals; unknownTimeErrors retains only Error/Fatal/Bug.}]]]
    if {$state ne ""} {
        lappend result $state
    }
    return $result
  • flush (scripted, public)

     <instance of nsmcp::CachedLogSearch[i]> flush

    Discard cached search state for this log source. Serialize the update and clean up retained result resources owned by the source.

    Returns:
    The cache cleanup result.

    Testcases:
    No testcase defined.
    ns_mutex lock ${:mutex}
    try {
        set key ${:cacheKey}:search
        if {[nsv_exists ${:store} $key]} {
            set meta [nsv_get ${:store} $key]
            :removeQueries [dict get $meta queries]
            dict set meta queries {}
            dict incr meta generation
            nsv_set ${:store} $key $meta
        }
    } finally {
        ns_mutex unlock ${:mutex}
    }
  • results (scripted, public)

     <instance of nsmcp::CachedLogSearch[i]> results handle [ cursor ] \
        [ limit ]

    Retrieve a bounded page from an unexpired cached search. Validate the handle and page bounds under the mutex and retain coverage and truncation metadata.

    Parameters:
    handle (required)
    Retained job or conversation handle; ownership and expiry checks still apply.
    cursor (optional, defaults to "0")
    Zero-based cursor into retained results. Defaults to '0'.
    limit (optional, defaults to "20")
    Maximum number of records or entries to return. Defaults to '20'.
    Returns:
    The cached page dictionary.

    Testcases:
    No testcase defined.
    if {![regexp {^[0-9a-f]{64}$} $handle]} {
        error "invalid result handle"
    }
    if {![string is entier -strict $cursor] || $cursor < 0 ||
        ![string is integer -strict $limit] || $limit < 1 || $limit > 100} {
        error "expected a nonnegative cursor and a limit between 1 and 100"
    }
    ns_mutex lock ${:mutex}
    try {
        set key ${:cacheKey}:search
        set query {}
        if {[nsv_exists ${:store} $key]} {
            set meta [nsv_get ${:store} $key]
            dict for {q candidate} [dict get $meta queries] {
                if {[dict exists $candidate resultHandle] &&
                    [dict get $candidate resultHandle] eq $handle} {
                    set query $candidate
                    break
                }
            }
        }
        if {$query eq ""} {
            return -code error -errorcode {NSMCP RESULT UNKNOWN} "unknown or expired result handle"
        }
        # Result handles created before a live reload have no cap flag.
        set query [dict merge {limitReached false} $query]
        file stat ${:path} current
        set probe [open ${:path} rb]
        try {
            set prefix [read $probe [string length [dict get $meta prefix]]]
        } finally {
            close $probe
        }
        if {[list $current(dev) $current(ino)] ne [dict get $meta identity] ||
            $current(size) < [dict get $meta observedSize] || $prefix ne [dict get $meta prefix]} {
            error "result handle expired after log rotation; repeat the search"
        }
        set end [dict get $query fileBytes]
        if {$cursor > $end} {
            error "cursor is beyond the published results"
        }
        set f [open [dict get $query resultFile] rb]
        try {
            if {$cursor > 0} {
                seek $f [expr {$cursor-1}]
                if {[read $f 1] ne "\n"} {
                    error "cursor must be a record boundary"
                }
            }
            seek $f $cursor
            set records {}
            set bytes 0
            while {[tell $f] < $end && [llength $records] < $limit} {
                set start [tell $f]
                set line {}
                while {1} {
                    set position [tell $f]
                    set chunk [read $f [expr {min(65536,$end-$position,2097153-[string length $line])}]]
                    set newline [string first \n $chunk]
                    if {$newline >= 0} {
                        append line [string range $chunk 0 [expr {$newline-1}]]
                        seek $f [expr {$position+$newline+1}]
                        break
                    }
                    append line $chunk
                    if {[string length $line] >= 2097153} {
                        error "cached record exceeds the page byte limit"
                    }
                    if {$chunk eq "" || [tell $f] >= $end} {
                        error "incomplete result cache"
                    }
                }
                set length [expr {[tell $f]-$start}]
                if {$length > 2097152} {
                    error "cached record exceeds the page byte limit"
                }
                if {$bytes+$length > 2097152} {
                    seek $f $start
                    break
                }
                incr bytes $length
                lappend records [ns_json parse [encoding convertfrom utf-8 $line]]
            }
            set next [tell $f]
        } finally {
            close $f
        }
        set more [expr {$next < $end}]
        return [dict create resultHandle $handle records $records  nextCursor [expr {$more ? $next : ""}] more $more  resultCount [dict get $query resultCount]  scanIncomplete [dict get $query scanIncomplete] pending [dict get $query pending]  cacheFull [dict get $query cacheFull] limitReached [dict get $query limitReached] generation [dict get $meta generation]]
    } finally {
        ns_mutex unlock ${:mutex}
    }
  • search (scripted, public)

     <instance of nsmcp::CachedLogSearch[i]> search literal [ severity ] \
        [ correlationField ] [ maxResults ]

    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.
    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}
    }