- Methods: All Methods Documented Methods Hide Methods
- Source: Display Source Hide Source
- Variables: Show Variables Hide Variables
Class ::nsmcp::AccessAnalytics
::nsmcp::AccessAnalyticscreate ... \
[ -database:required database:required ] \
[ -evictionLimit:integer (default "64") ] \
[ -maxLineBytes:integer (default "65536") ] \
[ -maxReadBytes:integer (default "16777216") ] \
[ -maxRows:integer (default "2000000") ] \
[ -maxSeconds:integer (default "2") ] \
[ -mutex (default "") ] \
[ -parser (default "::nsmcp::accessRecordParser") ] \
[ -path:required path:required ] \
[ -retentionSeconds:double (default "86400") ]
Defined in /usr/local/ns/tcl/nsmcp/lib/access-analytics.tcl
Class Relations
::nx::Class create ::nsmcp::AccessAnalytics \ -superclass ::nsmcp::AccessLogIngestorMethods (to be applied on instances)
summary (scripted, public)
<instance of nsmcp::AccessAnalytics> summary options
Use the scanner coverage and a single snapshot for all aggregates. Hold a single SQLite snapshot for coverage and all aggregate queries.
- Parameters:
- options (required)
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { set :db [${:database} connect] set :batchRows 0 ${:db} execute {BEGIN} set options [:validateWindow $options] :peerSchema ${:db} set plain $options foreach key {path status pool peer windowSeconds includePeerAddresses} {dict unset plain $key} # Coverage and counts share the same read transaction. set result [:summaryLocked $plain] set filters {}; foreach key {path status pool peer} {if {[dict exists $options $key]} {dict set filters $key [dict get $options $key]}} dict set result filters $filters set db ${:db}; set from [dict get $result from]; set to [dict get $result to] set where {start >= $from AND start < $to} foreach key {path status pool peer} { if {[dict exists $options $key]} {set $key [dict get $options $key]; append where " AND $key = \$$key"} } $db execute "SELECT count(*) AS requestCount,coalesce(sum(bytes),0) AS responseBytes, coalesce(sum(fallback),0) AS fallbackCount,coalesce(sum(excluded),0) AS excludedPaths, count(run) AS timingCount,coalesce(sum(accept),0) AS acceptTotal, coalesce(sum(queue),0) AS queueTotal,coalesce(sum(filter),0) AS filterTotal, coalesce(sum(run),0) AS runTotal,coalesce(max(run),0) AS runMax FROM nsmcp_access_requests WHERE $where" row { foreach key $row(*) {dict set result $key $row($key)} } foreach {dimension column} {pools pool methods method statuses status paths path} { set counts {} # Bounded output, but count all matching rows in totals above. $db execute "SELECT $column AS label,count(*) AS n FROM nsmcp_access_requests WHERE $where GROUP BY $column ORDER BY n DESC LIMIT 1000" row { if {$dimension eq "paths" && $row(label) eq ""} continue dict set counts $row(label) $row(n) } dict set result $dimension $counts } set tracked 0 dict for {label count} [dict get $result paths] {incr tracked $count} dict set result excludedPaths [expr {[dict get $result requestCount]-$tracked}] set statusTimings {} $db execute "SELECT status,count(*) AS n,count(run) AS timed,coalesce(sum(filter),0) AS filters, coalesce(sum(run),0) AS runs,coalesce(max(run),0) AS maximum FROM nsmcp_access_requests WHERE $where GROUP BY status" row { lappend statusTimings [dict create status $row(status) requestCount $row(n) timingCount $row(timed) filterTotal $row(filters) runTotal $row(runs) runMax $row(maximum)] } dict set result statusTimings $statusTimings set unknownPeers [$db scalar "SELECT count(*) FROM nsmcp_access_requests WHERE $where AND peer = ''"] dict set result coverage unknownPeerCount $unknownPeers dict set result coverage peerCountsComplete [expr {$unknownPeers==0 && [dict get $result coverage windowComplete]}] if {[dict exists $options windowSeconds]} { # RANGE counts all equal timestamps together and uses an inclusive # [end-windowSeconds,end] range clipped to the selected period. # SQLite performs the rolling calculation rather than Tcl fetching rows. set seconds [dict get $options windowSeconds] set burst [dict create windowSeconds $seconds requestCount 0 from {} to {}] $db execute "SELECT peer,start,n FROM ( SELECT peer,start,count(*) OVER ( PARTITION BY peer ORDER BY start RANGE BETWEEN \$seconds PRECEDING AND CURRENT ROW) AS n FROM nsmcp_access_requests WHERE $where AND peer != '' ) ORDER BY n DESC,start,peer LIMIT 1" row { dict set burst requestCount $row(n) dict set burst from [expr {max(double($from),$row(start)-$seconds)}] dict set burst to $row(start) if {[dict exists $options includePeerAddresses] && [dict get $options includePeerAddresses]} { dict set burst peerAddress $row(peer) } } dict set result peerBurst $burst } set size [dict get $result bucketSeconds]; set counts [dict get $result buckets] $db execute "SELECT cast(start/\$size AS INTEGER)*\$size AS slot,count(*) AS n FROM nsmcp_access_requests WHERE $where GROUP BY slot" row { dict set counts $row(slot) $row(n) } dict set result buckets $counts dict set result pathCountsComplete [expr {[dict get $result excludedPaths]==0}] set retained [$db scalar {SELECT coalesce(min(start),'') FROM nsmcp_access_requests}] set through [$db scalar {SELECT value FROM nsmcp_access_metadata WHERE key='droppedThrough'}] set evictionAt [$db scalar {SELECT value FROM nsmcp_access_metadata WHERE key='lastEvictionAt'}] dict set result coverage source ${:path} dict set result coverage retainedRequestFrom $retained dict set result coverage droppedThrough $through dict set result coverage lastEvictionAt $evictionAt set evicted [expr {$through ne "" && $from <= $through}] dict set result coverage evictionOverlap $evicted if {$evicted} {dict set result coverage windowComplete false; set reasons [dict get $result coverage incompleteReasons]; lappend reasons eviction; dict set result coverage incompleteReasons $reasons} dict set result coverage peerCountsComplete [expr {$unknownPeers==0 && [dict get $result coverage windowComplete]}] dict set result coverage databaseBytes [file size [${:database} path]] dict set result coverage storageBudgetBytes [${:database} cget -maxBytes] set events {} $db execute {SELECT * FROM nsmcp_access_evictions ORDER BY id DESC} row { lappend events [dict create droppedAt $row(dropped_at) from $row(first_time) to $row(last_time) rowCount $row(rows) reason $row(reason)] } dict set result evictions $events ${:db} execute {COMMIT} return $result } finally {if {[info exists :db]} {${:db} close; unset :db}; unset -nocomplain :batchRows; ns_mutex unlock ${:mutex}}validateWindow (scripted, public)
<instance of nsmcp::AccessAnalytics> validateWindow options
- Parameters:
- options (required)
- Testcases:
- No testcase defined.
set extra {} foreach key {path status pool peer windowSeconds includePeerAddresses} { if {![dict exists $options $key]} continue set value [dict get $options $key] if {$key eq "windowSeconds"} { if {![string is entier -strict $value] || $value<1 || $value>3600} {error "windowSeconds must be between 1 and 3600"} } elseif {$key eq "includePeerAddresses"} { if {![string is boolean -strict $value]} {error "includePeerAddresses must be boolean"} } elseif {$key eq "peer"} { if {![ns_ip valid $value]} {error "peer must be a valid IP address"} } elseif {$key eq "status"} { if {![string is entier -strict $value] || $value<100 || $value>599} {error "status must be between 100 and 599"} } elseif {$value eq "" || [string length $value]>512 || [string first \x00 $value]>=0} {error "invalid exact path or pool filter"} dict set extra $key $value; dict unset options $key } return [dict merge [next [list $options]] $extra]
- Methods: All Methods Documented Methods Hide Methods
- Source: Display Source Hide Source
- Variables: Show Variables Hide Variables
![[i]](/resources/acs-subsite/ZoomIn16.gif)