Class ::nsmcp::ServerHistory (public)

 ::nx::Class ::nsmcp::ServerHistory[i]

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

Testcases:
No testcase defined.
Source code:
    :property {name server_history}
    :property {description {Query persisted ns_stats intervals for the authenticated server. For peak analysis use mode summary: up to 1000 samples per call aggregated into at most 128 metric series with ranges, unweighted sample means and peak interval bounds; cumulative counters are excluded. more=true requires following nextArguments; never treat sampling rates as one-second peaks. mode records (default) retains raw paging. Defaults to the last 10 completed minutes, with 100 records per page. Use lastMinutes, period previous-hour, or explicit from/to (Unix seconds; half-open interval, at most 24 hours). Return records, referenced topologies, persistence status and coverage including gaps, restarts, evictions and write failures. Never assume complete coverage from record count. Follow nextArguments unchanged for stable paging. Intermediate coverage describes a page; final coverage with windowExamined=true examines the entire requested window independently of cursor, including earlier-page gaps and resets. Inspect complete and incompleteReasons. storage reports global oldest/newest retained times, sample count and SQLite used/allocated bytes for retention sizing. Does not enable sampling or persistence. Requires separately configured ns_stats SQLite history. Process metrics remain process-wide.}}
    :property {inputSchema {
        {
            "type": "object",
            "properties": {
                "mode": {"type": "string", "enum": ["records", "summary"], "default": "records"},
                "lastMinutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1440,
                    "default": 10
                },
                "period": {
                    "type": "string",
                    "enum": [
                        "previous-hour"
                    ]
                },
                "from": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Inclusive Unix epoch seconds"
                },
                "to": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Exclusive Unix epoch seconds"
                },
                "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 100
                },
                "cursor": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Paging timestamp in microseconds; requires explicit from/to"
                }
            },
            "additionalProperties": false
        }
    }}

    :method options {arguments} {
        if {[lindex $arguments 1] ne "object"} {
            error "Expected arguments object"
        }
        set options {limit 100 cursor 0 mode records}
        foreach {key type value} [lindex $arguments 2] {
            if {$key eq "mode"} {
                if {$type ne "string" || $value ni {records summary}} {error "mode must be records or summary"}
            } elseif {$key eq "period"} {
                if {$type ne "string" || $value ne "previous-hour"} {
                    error "period must be previous-hour"
                }
            } elseif {$key ni {lastMinutes from to limit cursor} || $type ne "number" || ![string is entier -strict $value]} {
                error "Supply integer time and paging options, or period previous-hour"
            }
            dict set options $key $value
        }
        set explicit [expr {[dict exists $options from] || [dict exists $options to]}]
        set relative [expr {[dict exists $options lastMinutes] + [dict exists $options period]}]
        if {$relative > 1 || ($explicit && $relative)} {
            error "Use lastMinutes, period, or paired from/to"
        }
        if {$explicit && (![dict exists $options from] || ![dict exists $options to])} {
            error "Supply both from and to"
        }
        if {!$explicit && [dict get $options cursor] != 0} {
            error "Paging requires the returned explicit from/to bounds"
        }
        set now [clock seconds]
        if {[dict exists $options period]} {
            set end [expr {($now / 3600) * 3600}]
            dict set options from [expr {$end - 3600}]
            dict set options to $end
            dict unset options period
        } elseif {!$explicit} {
            set minutes 10
            if {[dict exists $options lastMinutes]} {
                set minutes [dict get $options lastMinutes]
                dict unset options lastMinutes
            }
            if {$minutes < 1 || $minutes > 1440} {
                error "lastMinutes must be 1..1440"
            }
            set end [expr {($now / 60) * 60}]
            dict set options from [expr {$end - $minutes * 60}]
            dict set options to $end
        }
        dict with options {}
        if {$from < 0 || $to <= $from || $to > $now || $to - $from > 86400
            || $limit < 1 || $limit > 100 || $cursor < 0
            || ($cursor != 0 && ($cursor < $from * 1000000 || $cursor >= $to * 1000000))} {
            error "Expected a past window up to 24 hours, limit 1..100 and a cursor within the window"
        }
        if {[dict get $options mode] eq "summary" && [dict get $options limit] != 100} {
            error "limit applies to records mode; summary uses fixed bounded pages of 100"
        }
        return $options
    }

    :public method validate {arguments} {
        if {[catch {:options $arguments} message]} {
            return -code error -errorcode {NSMCP RPC -32602} $message
        }
    }

    :method compactHistory {context options} {
        # Bounded paging stays inside the server: raw topology/sample payloads
        # are never sent to the model in summary mode. Each call covers at most
        # 1000 samples; continuation explicitly describes the remaining window.
        set from [dict get $options from]; set to [dict get $options to]
        set cursor [dict get $options cursor]; set initialCursor $cursor
        set metrics {}; set samples 0; set unusable 0; set boundary 0
        set omitted 0; set pages 0
        for {set page 0} {$page < 10} {incr page} {
            set h [${:command} history query -server [$context server] -from $from -to $to -limit 100 -cursor $cursor]
            incr pages
            foreach record [dict get $h records] {
                incr samples
                set usable true
                foreach key {ready from to elapsedSeconds} {
                    if {![dict exists $record sampling $key] || [dict get $record sampling $key] eq ""} {set usable false}
                }
                if {!$usable || ![dict get $record sampling ready]
                    || ([dict exists $record sampling reset] && [dict get $record sampling reset])
                    || [dict get $record sampling elapsedSeconds] <= 0} {incr unusable; continue}
                set a [dict get $record sampling from]; set b [dict get $record sampling to]
                if {$a < $from || $b > $to} {incr boundary}
                set groups {}
                if {[dict exists $record servers [$context server] pools]} {
                    set totalRate 0.0; set totalKnown true; set poolCount 0
                    dict for {pool entry} [dict get $record servers [$context server] pools] {
                        incr poolCount
                        if {![dict exists $entry rates requests] || ![string is double -strict [dict get $entry rates requests]]
                            || [catch {expr {abs([dict get $entry rates requests]) < 1e300}} finite] || !$finite} {
                            set totalKnown false
                        } else {set totalRate [expr {$totalRate+[dict get $entry rates requests]}]}
                        # Never summarize cumulative counters as interval work.
                        foreach kind {rates gauges timingAverageSeconds} {
                            if {[dict exists $entry $kind]} {lappend groups [list [list servers [$context server] pools $pool $kind] [dict get $entry $kind]]}
                        }
                    }
                }
                if {[info exists poolCount] && $poolCount > 0} {
                    lappend groups [list [list servers [$context server] allPools rates] [dict create requests [expr {$totalKnown ? $totalRate : ""}]]]
                }
                unset -nocomplain poolCount
                if {[dict exists $record cpuPercent groups]} {lappend groups [list {cpuPercent groups} [dict get $record cpuPercent groups]]}
                foreach group $groups {
                    lassign $group prefix values
                    dict for {name value} $values {
                        set path [concat $prefix [list $name]]
                        if {![dict exists $metrics $path]} {
                            if {[dict size $metrics] >= 128} {incr omitted; continue}
                            dict set metrics $path [dict create count 0 unknown 0 sum 0.0 min {} max {} peakFrom {} peakTo {}]
                        }
                        set row [dict get $metrics $path]
                        # Missing and nonfinite values are unknown, never zero.
                        if {![string is double -strict $value] || [catch {expr {abs($value) < 1e300}} finite] || !$finite} {
                            dict incr row unknown
                        } else {
                            dict incr row count
                            dict set row sum [expr {[dict get $row sum]+$value}]
                            if {[dict get $row min] eq "" || $value < [dict get $row min]} {dict set row min $value}
                            if {[dict get $row max] eq "" || $value > [dict get $row max]} {
                                dict set row max $value; dict set row peakFrom $a; dict set row peakTo $b
                            }
                        }
                        dict set metrics $path $row
                    }
                }
            }
            if {![dict get $h more]} break
            set next [dict get $h nextCursor]
            if {![string is entier -strict $next] || $next <= $cursor} {error "History cursor did not advance"}
            set cursor $next
        }
        set rows {}; set i 0
        dict for {path row} $metrics {
            set fields {}; set parts {}; set j 0
            foreach part $path {lappend parts $j string $part; incr j}
            lappend fields path array $parts
            foreach key {count unknown min max peakFrom peakTo} {
                set value [dict get $row $key]
                lappend fields $key [expr {$value eq "" ? "null" : "number"}] $value
            }
            # Missing fields in other usable records are accounted separately
            # from explicit nulls; topology changes need not be parser errors.
            lappend fields absentSamples number [expr {$samples-$unusable-[dict get $row count]-[dict get $row unknown]}]
            set mean [expr {[dict get $row count] ? [dict get $row sum]/double([dict get $row count]) : ""}]
            lappend fields sampleMean [expr {$mean eq "" ? "null" : "number"}] $mean
            lappend rows $i object $fields; incr i
        }
        set more [dict get $h more]
        set summary [list from number $from to number $to initialCursor number $initialCursor  sampleCount number $samples unusableSamples number $unusable boundaryOverlapSamples number $boundary  pagesExamined number $pages more boolean $more metrics array $rows  omittedMetricObservations number $omitted  coverage object [:encode coverage [dict get $h coverage]]  limitations string {Metrics summarize retained sampling intervals, not instantaneous or one-second peaks. Rates and timings are supplied by ns_stats; sampleMean is unweighted, not a request-weighted latency. Peak bounds may overlap the requested window. Cumulative counters are excluded. Missing, reset and not-ready samples are not zero. CPU groups remain process-wide. At most 1000 samples and 128 metric series per call; missing series can reflect topology changes. Coverage is native history coverage; more=true requires continuation before claiming the window was examined.}]
        if {[dict exists $h storage]} {lappend summary storage object [:encode metrics [dict get $h storage]]}
        set nextFields {}
        if {$more} {set nextFields [list from number $from to number $to cursor number $cursor mode string summary]}
        return [list $summary $nextFields]
    }

    :public method execute {context arguments} {
        if {![:available]} {
            return [:unavailable $context]
        }
        set options [:options $arguments]
        set status [${:command} history status]
        set configured [dict exists $status pool]
        set fields [list server string [$context server] available boolean true  configured boolean $configured persistence object [:encode metrics $status]]
        if {!$configured} {
            lappend fields reason string {Configure ns_stats history with a dedicated nsdbsqlite pool to retain interval samples.}
            return [list {} object $fields]
        }
        if {[dict get $options mode] eq "summary"} {
            lassign [:compactHistory $context $options] summary nextFields
            lappend fields summary object $summary nextArguments [expr {$nextFields eq "" ? "null" : "object"}] $nextFields
            return [list {} object $fields]
        }
        dict unset options mode
        set query [list ${:command} history query -server [$context server]]
        dict for {key value} $options {
            lappend query -$key $value
        }
        set history [{*}$query]
        lappend fields history object [:encode history $history]
        if {[dict get $history more]} {
            set next [list from number [dict get $history from] to number [dict get $history to]  limit number [dict get $options limit] cursor number [dict get $history nextCursor]]
            lappend fields nextArguments object $next
        } else {
            lappend fields nextArguments null {}
        }
        return [list {} object $fields]
    }
XQL Not present:
Generic, PostgreSQL, Oracle
[ hide source ] | [ make this the default ]
Show another procedure: