nsmcp::AccessDiagnosticTool method page (public)

 <instance of nsmcp::AccessDiagnosticTool[i]> page context arguments \
    [ floor ] [ aggregationVar ]

Defined in /usr/local/ns/tcl/nsmcp/traffic-tools.tcl

Build one bounded routing-diagnostic evidence page. Validate options and authorize the requested evidence; optionally update the caller's aggregation variable.

Parameters:
context (required)
Authenticated request context carrying the principal, grants and selected server.
arguments (required)
Rooted ns_json typed triples for the tool's JSON argument object.
floor (optional, defaults to "0")
Minimum candidate position used by the caller's paging operation. Defaults to '0'.
aggregationVar (optional)
Optional caller variable receiving traffic aggregation state.
Returns:
The rooted JSON evidence page.

Testcases:
No testcase defined.
Source code:
if {$aggregationVar ne ""} {
    upvar 1 $aggregationVar traffic
}
set options [:options $arguments]
set server [$context server]
set peers [dict get $options includePeerAddresses]
if {$peers} {
    ${:authorizer} require $context diagnostics.logs.peers.read
}
set redactor [expr {$peers ? ${:peerRedactor} : ${:redactor}}]
set result [list server string $server from number [dict get $options from] to number [dict get $options to]]
foreach key {path authenticationState} {
    if {[dict exists $options $key]} {
        lappend result $key string [dict get $options $key]
    }
}
set snapshot {}
set maps {}
set mappingIdentity {}
set lookupMode fallback-rule-membership
set routeCache {}
if {${:operation} eq "bot"} {
    ${:authorizer} require $context diagnostics.runtime.read
    set snapshot [${:routing} snapshot $server]
    set target [dict get $options pool]
    if {![dict exists $snapshot pools $target]} {
        return [list {} object [concat $result [list available boolean false reason string {Effective target-pool mappings unavailable.}]]]
    }
    set maps [dict get $snapshot pools $target]
    if {[${:routing} contextSupported $server]} {
        set lookupMode native-context
    }
    set category [dict get $options category]
    if {$category ne "" && $category ne "unknown"} {
        set nativeCategory [expr {$category in {
            native-target-in-target native-target-other-pool-candidate
            bot-like-routed-elsewhere-heuristic native-other-pool
        }}]
        if {$nativeCategory != ($lookupMode eq "native-context")} {
            return -code error -errorcode {NSMCP RPC -32602}  "Category '$category' is not produced by lookupMode '$lookupMode'; select a category for the current mode."
        }
    }
    set mappingIdentity [ns_crypto::md string -digest sha256 -- [list [dict get $snapshot pools] $lookupMode]]
    if {[dict exists $options mappingIdentity] && [dict get $options mappingIdentity] ne $mappingIdentity} {
        error "Effective mappings changed; start a new analysis"
    }
    lappend result pool string $target lookupMode string $lookupMode mappingIdentity string $mappingIdentity {*}[${:routing} encodeSnapshot $snapshot]
}
if {${:operation} eq "slow"} {
    set thresholds {}
    foreach key {slowlogconntime slowlogfiltertime slowlogqueuetime} {
        set value [ns_config ns/server/$server/module/[dict get $options module] $key {}]
        lappend thresholds $key string $value
    }
    lappend result configuredThresholds object $thresholds
}
set path [:source $server $options]
if {$path eq "" || ![file isfile $path] || ![file readable $path]} {
    return [list {} object [concat $result [list available boolean false reason string {Configured log is missing, disabled or unreadable.}]]]
}
set path [file normalize $path]
file stat $path before
set channel [open $path rb]
try {
    set prefix [read $channel [expr {min(256,$before(size))}]]
    set identity [ns_crypto::md string -digest sha256 -- [list $path $before(dev) $before(ino) $prefix]]
    set sourceBytes $before(size)
    set end $sourceBytes
    if {[dict exists $options beforeOffset]} {
        if {$identity ne [dict get $options sourceIdentity] || $sourceBytes<[dict get $options sourceBytes]} {
            error "Log rotated or truncated; start a new query"
        }
        set sourceBytes [dict get $options sourceBytes]
        set end [dict get $options beforeOffset]
    }
    set start [expr {max($floor,$end-${:maxReadBytes})}]
    seek $channel $start
    set bytes [read $channel [expr {$end-$start}]]
    set discarded 0
    if {$start>0} {
        seek $channel [expr {$start-1}]
        if {[read $channel 1] ne "\n"} {
            set newline [string first \n $bytes]
            if {$newline>=0} {
                set discarded [expr {$newline+1}]
            } else {
                set discarded [string length $bytes]
            }
        }
    }
    file stat $path after
    if {[list $before(dev) $before(ino)] ne [list $after(dev) $after(ino)] || $after(size)<$sourceBytes} {
        error "Log changed during read; start a new query"
    }
} finally {
    close $channel
}
set position [expr {$start+$discarded}]
set cursor $position
if {$cursor==$end && $start>0} {
    set cursor $start
}
set bytes [string range $bytes $discarded end]
set pending [expr {$bytes ne "" && [string index $bytes end] ne "\n"}]
set records {}
set groups {}
set totals {}
set matches 0
set unrecognized 0
set oversized 0
set missingUA 0
set missingPool 0
set selectedMatches 0
foreach line [lrange [split $bytes \n] 0 end-1] {
    set offset $position
    incr position [expr {[string length $line]+1}]
    if {[string length $line]>65536} {
        incr oversized
        continue
    }
    set record [::nsmcp::accessRecordParser parse [encoding convertfrom utf-8 [string trimright $line \r]]]
    if {$record eq ""} {
        incr unrecognized
        continue
    }
    set at [dict get $record startTime]
    if {$at<[dict get $options from] || $at>=[dict get $options to]} {
        continue
    }
    if {[dict exists $options path] && [dict get $record path] ne [dict get $options path]} {
        continue
    }
    if {[dict exists $options authenticationState] && [dict get $record authenticationState] ne [dict get $options authenticationState]} {
        continue
    }
    incr matches
    dict set record offset $offset
    dict set record endOffset $position
    if {![dict exists $record userAgent]} {
        incr missingUA
    }
    if {[dict get $record pool] eq "unknown"} {
        incr missingPool
    }
    if {${:operation} eq "bot"} {
        if {$lookupMode eq "native-context"} {
            set routeKey [list [dict get $record method] [dict get $record path]  [dict get $record peerAddress] [dict get $record pool]  [dict exists $record userAgent] [:dict getdef $record userAgent ""]]
            if {[dict exists $routeCache $routeKey]} {
                set classification [dict get $routeCache $routeKey]
            } else {
                set classification [:nativeClassify $server $record $snapshot [dict get $options pool]]
                if {[dict size $routeCache]<4096} {
                    dict set routeCache $routeKey $classification
                }
            }
        } else {
            set classification [:classify $record $maps [dict get $options pool]]
            dict set classification lookupMode fallback-rule-membership
            dict set classification expectedPool unknown
            dict set classification contextComplete false
            dict set classification lookupError {}
        }
        set category [dict get $classification category]
        dict incr totals $category
        if {[dict get $options category] ne "" && [dict get $options category] ne $category} {
            continue
        }
        incr selectedMatches
        if {$aggregationVar ne ""} {
            :accumulateTraffic traffic $record
        }
        # Group only after peer and URL redaction. Omitted groups still
        # contribute to category totals and omittedRecords.
        set record [$redactor record $record]
        if {[dict exists $record userAgent]} {
            dict set record userAgent [$redactor message [dict get $record userAgent]]
        }
        set record [dict merge $record $classification]
        set key [list [dict get $record category] [dict get $record method] [dict get $record path] [dict get $record pool] [dict get $record status]]
        lappend key [:dict getdef $record userAgent "unknown"]
        lappend key [dict get $record expectedPool] [dict get $record contextComplete] [dict get $record lookupError]
        if {$peers} {
            lappend key [dict get $record peerAddress]
        }
        if {![dict exists $groups $key]} {
            if {[dict size $groups]>=[dict get $options limit]} {
                set priority [:evidencePriority $category]
                set victim {}
                set worst $priority
                dict for {existingKey existingGroup} $groups {
                    set rank [:evidencePriority [dict get $existingGroup category]]
                    if {$rank>$worst} {
                        set worst $rank
                        set victim $existingKey
                    }
                }
                if {$victim eq ""} {
                    continue
                }
                dict unset groups $victim
            }
            set group [dict create method [dict get $record method] path [$redactor queryValues [dict get $record path]] pool [dict get $record pool] status [dict get $record status] userAgent [lindex $key 5] category [dict get $record category] matchingRuleIndexes [dict get $record matchingRuleIndexes] unknownRules [dict get $record unknownRules] sampleRequestIdentifier [dict get $record requestIdentifier] firstAt $at lastAt $at count 0 runTotal 0 runMax 0 timingCount 0]
            foreach field {lookupMode expectedPool contextComplete lookupError} {
                dict set group $field [dict get $record $field]
            }
            if {$peers} {
                dict set group peerAddress [dict get $record peerAddress]
            }
            dict set groups $key $group
        }
        set group [dict get $groups $key]
        dict incr group count
        dict set group firstAt [expr {min([dict get $group firstAt],$at)}]
        dict set group lastAt [expr {max([dict get $group lastAt],$at)}]
        if {[dict exists $record runTime]} {
            dict incr group timingCount
            dict set group runMax [expr {max([dict get $group runMax],[dict get $record runTime])}]
            dict set group runTotal [expr {[dict get $group runTotal]+[dict get $record runTime]}]
        }
        dict set groups $key $group
    } else {
        set record [$redactor record $record]
        if {[dict exists $record userAgent]} {
            dict set record userAgent [$redactor message [dict get $record userAgent]]
        }
        lappend records $record
        set records [lrange $records end-[expr {[dict get $options limit]-1}] end]
    }
}
if {${:operation} eq "bot"} {
    set latest [${:routing} snapshot $server]
    set latestIdentity [ns_crypto::md string -digest sha256 -- [list [dict get $latest pools] $lookupMode]]
    if {$latestIdentity ne $mappingIdentity} {
        error "Effective mappings changed during analysis; start a new query"
    }
}
set entries {}
set i 0
if {${:operation} eq "bot"} {
    set ordered {}
    dict for {key group} $groups {
        lappend ordered [list [:evidencePriority [dict get $group category]] $group]
    }
    # The common output loop reverses records for slow-log chronology.
    set records {}
    foreach pair [lsort -integer -index 0 $ordered] {
        lappend records [lindex $pair 1]
    }
    set records [lreverse $records]
} else {
    set selectedMatches $matches
}
set returnedRecords {}
set resultBytes 0
foreach record [lreverse $records] {
    set fields [:recordFields $record]
    set bytes [string length [ns_json value -- [list {} object $fields]]]
    if {$resultBytes+$bytes>65536} {
        continue
    }
    incr resultBytes $bytes
    lappend returnedRecords $record
    lappend entries $i object $fields
    incr i
}
set records $returnedRecords
set counts {}
dict for {key value} $totals {
    lappend counts $key number $value
}
set omittedCounts {}
set returnedMatches [llength $records]
if {${:operation} eq "bot"} {
    set remaining {}
    dict for {category value} $totals {
        if {[dict get $options category] eq "" || [dict get $options category] eq $category} {
            dict set remaining $category $value
        }
    }
    set returnedMatches 0
    foreach record $records {
        set count [dict get $record count]
        incr returnedMatches $count
        dict incr remaining [dict get $record category] [expr {-$count}]
    }
    dict for {category value} $remaining {
        lappend omittedCounts $category number $value
    }
}
set next {}
if {$cursor>$floor} {
    foreach key {from to limit pool module includePeerAddresses mode} {
        lappend next $key [expr {$key eq "includePeerAddresses" ? "boolean" : ($key in {from to limit} ? "number" : "string")}] [dict get $options $key]
    }
    if {${:operation} eq "bot" && [dict get $options category] ne ""} {
        lappend next category string [dict get $options category]
    }
    foreach key {path authenticationState} {
        if {[dict exists $options $key]} {
            lappend next $key string [dict get $options $key]
        }
    }
    lappend next beforeOffset number $cursor sourceBytes number $sourceBytes sourceIdentity string $identity
    if {$mappingIdentity ne ""} {
        lappend next mappingIdentity string $mappingIdentity
    }
}
set evidencePolicy {Newest matching slow-request records within evidence limits.}
if {${:operation} eq "bot"} {
    set evidencePolicy {Routing candidates first, then unknowns, then routine groups; first encounter within priority. Category filtering selects evidence only; categoryTotals still includes all observed records.}
}
lappend result available boolean true sourcePath string $path sourceIdentity string $identity  matches number $matches categoryTotals object $counts records array $entries  selectedCategory string [dict get $options category] selectedMatches number $selectedMatches  filteredOutRecords number [expr {$matches-$selectedMatches}]  omittedRecords number [expr {$selectedMatches-$returnedMatches}] omittedCategoryTotals object $omittedCounts  evidencePolicy string $evidencePolicy  more boolean [expr {$cursor>$floor}] nextArguments object $next  coverage object [list sourceBytes number $sourceBytes examinedFromOffset number [expr {$start+$discarded}] examinedToOffset number $end  unreadEarlierBytes number $cursor pendingFinalRecord boolean $pending unrecognizedRecords number $unrecognized oversizedRecords number $oversized  missingUserAgent number $missingUA missingPool number $missingPool rotationsExamined boolean false windowComplete boolean false  limitations string {Page-local counts; sum disjoint pages. Current file only. Full-window coverage is not certified. Mappings reflect query time, not historical configuration. lookupMode identifies native-context or fallback-rule-membership. Native lookup uses current URL/path precedence and logged User-Agent and valid peer IP, without disclosing peers by default. Unlogged context predicates remain unknown; completeness checks conservatively inspect all same-method rules. Older versions retain root wildcard User-Agent membership analysis, not precedence replay. Bot-like unmatched uses a separate case-insensitive heuristic, not proof of a bot. Slow-log timings are access-log timings, not SQL profiles.}]
return [list {} object $result]
XQL Not present:
Generic, PostgreSQL, Oracle
[ hide source ] | [ make this the default ]
Show another procedure: