Class ::nsmcp::AccessAnalytics

::nsmcp::AccessAnalytics[i] create ... \
           [ -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

  • class: ::nx::Class[i]
  • superclass: ::nsmcp::AccessLogIngestor[i]
::nx::Class create ::nsmcp::AccessAnalytics \
     -superclass ::nsmcp::AccessLogIngestor

Methods (to be applied on instances)

  • summary (scripted, public)

     <instance of nsmcp::AccessAnalytics[i]> 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[i]> 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]