::nsmcp::AccessAnalytics ::nsmcp::AccessAnalytics accumulate drop ingestLocked init maintain peerSchema publish rejected state summary validateWindow ::nsmcp::AccessLogIngestor ::nsmcp::AccessLogIngestor ingest ingestLocked init initial locateRecent recentState refresh retentionCutoff sample summaryLocked validateWindow ::nsmcp::AccessAnalytics->::nsmcp::AccessLogIngestor ::nx::Object ::nx::Object __default_accessor __default_method_call_protection __object_configureparameter __resolve_method_path contains copy delete object method delete object property delete object variable destroy_on_cleanup info consts info info info lookup parameters info lookup slots info lookup syntax info lookup variables info object method args info object method body info object method callprotection info object method debug info object method definition info object method definitionhandle info object method deprecated info object method exists info object method handle info object method origin info object method parameters info object method registrationhandle info object method returns info object method submethods info object method syntax info object method type info object slots info object variables info variable definition info variable name info variable parameter move object alias object forward object method object property object variable private protected public qn require namespace require object method require private object method require protected object method require public object method serialize ::nsmcp::AccessLogIngestor->::nx::Object

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]