::nx::Object ::nx::Object ::nsmcp::LogService ::nsmcp::LogService init owned poll require resolveWindow results run start summary ::nsmcp::LogService->::nx::Object

Class ::nsmcp::LogService

::nsmcp::LogService[i] create ... \
           [ -analytics (default "") ] \
           [ -authorizer:required authorizer:required ] \
           [ -correlation:required correlation:required ] \
           [ -lifetime:integer (default "900") ] \
           [ -maxJobs:integer (default "128") ] \
           [ -mutex mutex ] \
           [ -peerRedactor:required peerRedactor:required ] \
           [ -queue:required queue:required ] \
           [ -redactor:required redactor:required ] \
           [ -store:required store:required ]

Defined in /usr/local/ns/tcl/nsmcp/lib/log-tools.tcl

Class Relations

  • class: ::nx::Class[i]
  • superclass: ::nx::Object[i]
::nx::Class create ::nsmcp::LogService \
     -superclass ::nx::Object

Methods (to be applied on instances)

  • poll (scripted, public)

     <instance of nsmcp::LogService[i]> poll context handle
    Parameters:
    context (required)
    handle (required)

    Testcases:
    No testcase defined.
    :require $context
    ns_mutex lock ${:mutex}
    try {
        if {![nsv_exists ${:store} $handle] || ![:owned $context [nsv_get ${:store} $handle]]} {
            foreach key [nsv_array names ${:store}] {
                set record [nsv_get ${:store} $key]
                if {![:owned $context $record]} continue
                if {[dict exists $record mappings $handle]} {return -code error -errorcode {NSMCP RPC -32602} "This is a resultHandle; pass it as resultHandle to read a page"}
                if {[dict exists $record windowHandle] && [dict get $record windowHandle] eq $handle} {return -code error -errorcode {NSMCP RPC -32602} "This is a windowHandle; use it with access_summary"}
            }
            return -code error -errorcode {NSMCP RPC -32602} "Unknown or expired job handle"
        }
        set record [nsv_get ${:store} $handle]
        if {[dict exists $record requiresPeers] && [dict get $record requiresPeers]} {${:authorizer} require $context diagnostics.logs.peers.read}
        set data [list jobHandle string $handle status string [dict get $record status] server string [ns_info server]]
        if {[dict exists $record windowHandle]} {lappend data windowHandle string [dict get $record windowHandle]}
        if {[dict get $record status] eq "complete"} {lappend data result object [dict get $record data]}
        if {[dict get $record status] eq "failed"} {lappend data error string {Log search failed; consult the server log.}}
        return [list {} object $data]
    } finally {ns_mutex unlock ${:mutex}}
  • resolveWindow (scripted, public)

     <instance of nsmcp::LogService[i]> resolveWindow context handle
    Parameters:
    context (required)
    handle (required)

    Testcases:
    No testcase defined.
    :require $context
    ns_mutex lock ${:mutex}
    try {
        foreach key [nsv_array names ${:store}] {
            set record [nsv_get ${:store} $key]
            if {![:owned $context $record]} continue
            if {[dict exists $record windowHandle] && [dict get $record windowHandle] eq $handle} {return [dict get $record window]}
            if {$key eq $handle} {return -code error -errorcode {NSMCP RPC -32602} "This is a jobHandle; use its windowHandle for access_summary"}
            if {[dict exists $record mappings $handle]} {return -code error -errorcode {NSMCP RPC -32602} "This is a resultHandle; use it with log_results, not access_summary"}
        }
        return -code error -errorcode {NSMCP RPC -32602} "Unknown or expired windowHandle; start a new interval"
    } finally {ns_mutex unlock ${:mutex}}
  • results (scripted, public)

     <instance of nsmcp::LogService[i]> results context handle cursor \
        limit includePeers
    Parameters:
    context (required)
    handle (required)
    cursor (required)
    limit (required)
    includePeers (required)

    Testcases:
    No testcase defined.
    :require $context
    if {$includePeers} {${:authorizer} require $context diagnostics.logs.peers.read}
    set internal {}
    ns_mutex lock ${:mutex}
    try {
        foreach key [nsv_array names ${:store}] {
            set record [nsv_get ${:store} $key]
            if {[:owned $context $record] && [dict exists $record mappings $handle]} {
                set internal [dict get $record mappings $handle]; break
            }
        }
    } finally {ns_mutex unlock ${:mutex}}
    if {$internal eq ""} {
        ns_mutex lock ${:mutex}
        try {
            foreach key [nsv_array names ${:store}] {
                set record [nsv_get ${:store} $key]
                if {![:owned $context $record]} continue
                if {$key eq $handle} {return -code error -errorcode {NSMCP RPC -32602} "This is a jobHandle; pass it as jobHandle to poll the job"}
                if {[dict exists $record windowHandle] && [dict get $record windowHandle] eq $handle} {return -code error -errorcode {NSMCP RPC -32602} "This is a windowHandle; use it with access_summary"}
            }
        } finally {ns_mutex unlock ${:mutex}}
        return -code error -errorcode {NSMCP RPC -32602} "Unknown or expired result handle"
    }
    set page [${:correlation} results $internal $cursor $limit]
    set policy [expr {$includePeers ? ${:peerRedactor} : ${:redactor}}]
    set page [$policy page $page]
    set records {}; set index 0
    foreach entry [dict get $page records] {
        set fields {}
        foreach key {timestamp processThread logThread severity requestIdentifier peerAddress method url protocol message pool timingSource} {
            if {[dict exists $entry $key]} {lappend fields $key string [dict get $entry $key]}
        }
        foreach key {offset endOffset status responseBytes} {
            set value [dict get $entry $key]
            lappend fields $key [expr {$value eq "" ? "string" : "number"}] $value
        }
        foreach key {startTime acceptTime queueTime filterTime runTime} {
            if {[dict exists $entry $key]} {lappend fields $key number [dict get $entry $key]}
        }
        foreach key {complete truncated orphanContinuation redacted} {lappend fields $key boolean [dict get $entry $key]}
        lappend records $index object $fields
        incr index
    }
    set data [list server string [ns_info server] resultHandle string $handle records array $records nextCursor string [dict get $page nextCursor]]
    foreach key {resultCount generation} {lappend data $key number [dict get $page $key]}
    foreach key {more scanIncomplete pending cacheFull} {lappend data $key boolean [dict get $page $key]}
    return [list {} object $data]
  • run (scripted, public)

     <instance of nsmcp::LogService[i]> run handle operation identifier
    Parameters:
    handle (required)
    operation (required)
    identifier (required)

    Testcases:
    No testcase defined.
    ns_mutex lock ${:mutex}
    try {
        if {![nsv_exists ${:store} $handle]} return
        set record [nsv_get ${:store} $handle]
        dict set record status running
        nsv_set ${:store} $handle $record
    } finally {ns_mutex unlock ${:mutex}}
    set mappings {}
    try {
        if {$operation eq "errors"} {
            set data [:summary [${:correlation} errors] mappings]
        } elseif {$operation eq "accessSummary"} {
            if {${:analytics} eq ""} {error "access analytics is not configured"}
            ${:analytics} ingest [dict get $identifier from]
            set summary [${:analytics} summary $identifier]
            # Jobs queued before a live reload have no interval handle yet.
            if {![dict exists $record windowHandle]} {
                dict set record windowHandle [ns_crypto::randombytes -encoding hex 32]
                dict set record window [dict create from [dict get $identifier from] to [dict get $identifier to]]
            }
            dict set summary windowHandle [dict get $record windowHandle]
            set from [clock format [dict get $summary from] -timezone UTC -format {%Y-%m-%d %H:%M:%S}]
            set to [clock format [dict get $summary to] -timezone UTC -format {%Y-%m-%d %H:%M:%S}]
            dict set summary intervalUTC "$from to $to UTC"
            set data [::nsmcp::accessSummaryTriples $summary]
        } else {
            set context [${:correlation} requestContext $identifier]
            set data [list server string [ns_info server] requestIdentifier string $identifier system object [:summary [dict get $context system] mappings]]
            set access {}; set index 0
            dict for {source snapshot} [dict get $context access] {
                lappend access $index object [:summary $snapshot mappings]
                incr index
            }
            lappend data access array $access
        }
        dict set record status complete
        dict set record data $data
        dict set record mappings $mappings
    } on error {message options} {
        dict set record status failed
        # No source paths, log contents, or exception details in client errors.
        ns_log error "nsmcp: background log operation failed: $message"
    }
    ns_mutex lock ${:mutex}
    try {nsv_set ${:store} $handle $record} finally {ns_mutex unlock ${:mutex}}
  • start (scripted, public)

     <instance of nsmcp::LogService[i]> start context operation \
        [ identifier ]
    Parameters:
    context (required)
    operation (required)
    identifier (optional)

    Testcases:
    No testcase defined.
    :require $context
    if {$operation ni {errors requestContext accessSummary}} {error "invalid log operation"}
    if {$operation eq "requestContext" && ![::nsmcp::logRecordParser validRequestIdentifier $identifier]} {
        return -code error -errorcode {NSMCP RPC -32602} "Invalid request identifier"
    }
    set requiresPeers [expr {$operation eq "accessSummary" &&
        ([dict exists $identifier peer] || ([dict exists $identifier includePeerAddresses] && [dict get $identifier includePeerAddresses]))}]
    if {$requiresPeers} {${:authorizer} require $context diagnostics.logs.peers.read}
    ns_mutex lock ${:mutex}
    try {
        foreach key [nsv_array names ${:store}] {
            set record [nsv_get ${:store} $key]
            if {[dict get $record expires] <= [clock seconds] && [dict get $record status] in {complete failed}} {
                nsv_unset ${:store} $key
            }
        }
        if {[llength [nsv_array names ${:store}]] >= ${:maxJobs}} {error "log job limit reached"}
        set handle [ns_crypto::randombytes -encoding hex 32]
        set record [dict create principal [$context principal] server [$context server]  expires [expr {[clock seconds]+${:lifetime}}] status queued mappings {} requiresPeers $requiresPeers]
        set windowHandle {}
        if {$operation eq "accessSummary"} {
            set windowHandle [ns_crypto::randombytes -encoding hex 32]
            dict set record windowHandle $windowHandle
            dict set record window [dict create from [dict get $identifier from] to [dict get $identifier to]]
        }
        nsv_set ${:store} $handle $record
        try {
            ns_job queue -detached ${:queue} [list [self] run $handle $operation $identifier]
        } on error {message options} {
            nsv_unset ${:store} $handle
            return -options $options $message
        }
    } finally {ns_mutex unlock ${:mutex}}
    set data [list jobHandle string $handle status string queued server string [ns_info server]]
    if {$windowHandle ne ""} {lappend data windowHandle string $windowHandle}
    return [list {} object $data]