mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-12-30 14:00:57 +00:00
8176493bc0
Signed-off-by: Mustafa Gezen <mustafa@ctrliq.com>
638 lines
19 KiB
Go
638 lines
19 KiB
Go
// Copyright 2018 The Prometheus Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package procfs
|
|
|
|
// While implementing parsing of /proc/[pid]/mountstats, this blog was used
|
|
// heavily as a reference:
|
|
// https://utcc.utoronto.ca/~cks/space/blog/linux/NFSMountstatsIndex
|
|
//
|
|
// Special thanks to Chris Siebenmann for all of his posts explaining the
|
|
// various statistics available for NFS.
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Constants shared between multiple functions.
|
|
const (
|
|
deviceEntryLen = 8
|
|
|
|
fieldBytesLen = 8
|
|
fieldEventsLen = 27
|
|
|
|
statVersion10 = "1.0"
|
|
statVersion11 = "1.1"
|
|
|
|
fieldTransport10TCPLen = 10
|
|
fieldTransport10UDPLen = 7
|
|
|
|
fieldTransport11TCPLen = 13
|
|
fieldTransport11UDPLen = 10
|
|
)
|
|
|
|
// A Mount is a device mount parsed from /proc/[pid]/mountstats.
|
|
type Mount struct {
|
|
// Name of the device.
|
|
Device string
|
|
// The mount point of the device.
|
|
Mount string
|
|
// The filesystem type used by the device.
|
|
Type string
|
|
// If available additional statistics related to this Mount.
|
|
// Use a type assertion to determine if additional statistics are available.
|
|
Stats MountStats
|
|
}
|
|
|
|
// A MountStats is a type which contains detailed statistics for a specific
|
|
// type of Mount.
|
|
type MountStats interface {
|
|
mountStats()
|
|
}
|
|
|
|
// A MountStatsNFS is a MountStats implementation for NFSv3 and v4 mounts.
|
|
type MountStatsNFS struct {
|
|
// The version of statistics provided.
|
|
StatVersion string
|
|
// The mount options of the NFS mount.
|
|
Opts map[string]string
|
|
// The age of the NFS mount.
|
|
Age time.Duration
|
|
// Statistics related to byte counters for various operations.
|
|
Bytes NFSBytesStats
|
|
// Statistics related to various NFS event occurrences.
|
|
Events NFSEventsStats
|
|
// Statistics broken down by filesystem operation.
|
|
Operations []NFSOperationStats
|
|
// Statistics about the NFS RPC transport.
|
|
Transport NFSTransportStats
|
|
}
|
|
|
|
// mountStats implements MountStats.
|
|
func (m MountStatsNFS) mountStats() {}
|
|
|
|
// A NFSBytesStats contains statistics about the number of bytes read and written
|
|
// by an NFS client to and from an NFS server.
|
|
type NFSBytesStats struct {
|
|
// Number of bytes read using the read() syscall.
|
|
Read uint64
|
|
// Number of bytes written using the write() syscall.
|
|
Write uint64
|
|
// Number of bytes read using the read() syscall in O_DIRECT mode.
|
|
DirectRead uint64
|
|
// Number of bytes written using the write() syscall in O_DIRECT mode.
|
|
DirectWrite uint64
|
|
// Number of bytes read from the NFS server, in total.
|
|
ReadTotal uint64
|
|
// Number of bytes written to the NFS server, in total.
|
|
WriteTotal uint64
|
|
// Number of pages read directly via mmap()'d files.
|
|
ReadPages uint64
|
|
// Number of pages written directly via mmap()'d files.
|
|
WritePages uint64
|
|
}
|
|
|
|
// A NFSEventsStats contains statistics about NFS event occurrences.
|
|
type NFSEventsStats struct {
|
|
// Number of times cached inode attributes are re-validated from the server.
|
|
InodeRevalidate uint64
|
|
// Number of times cached dentry nodes are re-validated from the server.
|
|
DnodeRevalidate uint64
|
|
// Number of times an inode cache is cleared.
|
|
DataInvalidate uint64
|
|
// Number of times cached inode attributes are invalidated.
|
|
AttributeInvalidate uint64
|
|
// Number of times files or directories have been open()'d.
|
|
VFSOpen uint64
|
|
// Number of times a directory lookup has occurred.
|
|
VFSLookup uint64
|
|
// Number of times permissions have been checked.
|
|
VFSAccess uint64
|
|
// Number of updates (and potential writes) to pages.
|
|
VFSUpdatePage uint64
|
|
// Number of pages read directly via mmap()'d files.
|
|
VFSReadPage uint64
|
|
// Number of times a group of pages have been read.
|
|
VFSReadPages uint64
|
|
// Number of pages written directly via mmap()'d files.
|
|
VFSWritePage uint64
|
|
// Number of times a group of pages have been written.
|
|
VFSWritePages uint64
|
|
// Number of times directory entries have been read with getdents().
|
|
VFSGetdents uint64
|
|
// Number of times attributes have been set on inodes.
|
|
VFSSetattr uint64
|
|
// Number of pending writes that have been forcefully flushed to the server.
|
|
VFSFlush uint64
|
|
// Number of times fsync() has been called on directories and files.
|
|
VFSFsync uint64
|
|
// Number of times locking has been attempted on a file.
|
|
VFSLock uint64
|
|
// Number of times files have been closed and released.
|
|
VFSFileRelease uint64
|
|
// Unknown. Possibly unused.
|
|
CongestionWait uint64
|
|
// Number of times files have been truncated.
|
|
Truncation uint64
|
|
// Number of times a file has been grown due to writes beyond its existing end.
|
|
WriteExtension uint64
|
|
// Number of times a file was removed while still open by another process.
|
|
SillyRename uint64
|
|
// Number of times the NFS server gave less data than expected while reading.
|
|
ShortRead uint64
|
|
// Number of times the NFS server wrote less data than expected while writing.
|
|
ShortWrite uint64
|
|
// Number of times the NFS server indicated EJUKEBOX; retrieving data from
|
|
// offline storage.
|
|
JukeboxDelay uint64
|
|
// Number of NFS v4.1+ pNFS reads.
|
|
PNFSRead uint64
|
|
// Number of NFS v4.1+ pNFS writes.
|
|
PNFSWrite uint64
|
|
}
|
|
|
|
// A NFSOperationStats contains statistics for a single operation.
|
|
type NFSOperationStats struct {
|
|
// The name of the operation.
|
|
Operation string
|
|
// Number of requests performed for this operation.
|
|
Requests uint64
|
|
// Number of times an actual RPC request has been transmitted for this operation.
|
|
Transmissions uint64
|
|
// Number of times a request has had a major timeout.
|
|
MajorTimeouts uint64
|
|
// Number of bytes sent for this operation, including RPC headers and payload.
|
|
BytesSent uint64
|
|
// Number of bytes received for this operation, including RPC headers and payload.
|
|
BytesReceived uint64
|
|
// Duration all requests spent queued for transmission before they were sent.
|
|
CumulativeQueueMilliseconds uint64
|
|
// Duration it took to get a reply back after the request was transmitted.
|
|
CumulativeTotalResponseMilliseconds uint64
|
|
// Duration from when a request was enqueued to when it was completely handled.
|
|
CumulativeTotalRequestMilliseconds uint64
|
|
// The count of operations that complete with tk_status < 0. These statuses usually indicate error conditions.
|
|
Errors uint64
|
|
}
|
|
|
|
// A NFSTransportStats contains statistics for the NFS mount RPC requests and
|
|
// responses.
|
|
type NFSTransportStats struct {
|
|
// The transport protocol used for the NFS mount.
|
|
Protocol string
|
|
// The local port used for the NFS mount.
|
|
Port uint64
|
|
// Number of times the client has had to establish a connection from scratch
|
|
// to the NFS server.
|
|
Bind uint64
|
|
// Number of times the client has made a TCP connection to the NFS server.
|
|
Connect uint64
|
|
// Duration (in jiffies, a kernel internal unit of time) the NFS mount has
|
|
// spent waiting for connections to the server to be established.
|
|
ConnectIdleTime uint64
|
|
// Duration since the NFS mount last saw any RPC traffic.
|
|
IdleTimeSeconds uint64
|
|
// Number of RPC requests for this mount sent to the NFS server.
|
|
Sends uint64
|
|
// Number of RPC responses for this mount received from the NFS server.
|
|
Receives uint64
|
|
// Number of times the NFS server sent a response with a transaction ID
|
|
// unknown to this client.
|
|
BadTransactionIDs uint64
|
|
// A running counter, incremented on each request as the current difference
|
|
// ebetween sends and receives.
|
|
CumulativeActiveRequests uint64
|
|
// A running counter, incremented on each request by the current backlog
|
|
// queue size.
|
|
CumulativeBacklog uint64
|
|
|
|
// Stats below only available with stat version 1.1.
|
|
|
|
// Maximum number of simultaneously active RPC requests ever used.
|
|
MaximumRPCSlotsUsed uint64
|
|
// A running counter, incremented on each request as the current size of the
|
|
// sending queue.
|
|
CumulativeSendingQueue uint64
|
|
// A running counter, incremented on each request as the current size of the
|
|
// pending queue.
|
|
CumulativePendingQueue uint64
|
|
}
|
|
|
|
// parseMountStats parses a /proc/[pid]/mountstats file and returns a slice
|
|
// of Mount structures containing detailed information about each mount.
|
|
// If available, statistics for each mount are parsed as well.
|
|
func parseMountStats(r io.Reader) ([]*Mount, error) {
|
|
const (
|
|
device = "device"
|
|
statVersionPrefix = "statvers="
|
|
|
|
nfs3Type = "nfs"
|
|
nfs4Type = "nfs4"
|
|
)
|
|
|
|
var mounts []*Mount
|
|
|
|
s := bufio.NewScanner(r)
|
|
for s.Scan() {
|
|
// Only look for device entries in this function
|
|
ss := strings.Fields(string(s.Bytes()))
|
|
if len(ss) == 0 || ss[0] != device {
|
|
continue
|
|
}
|
|
|
|
m, err := parseMount(ss)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Does this mount also possess statistics information?
|
|
if len(ss) > deviceEntryLen {
|
|
// Only NFSv3 and v4 are supported for parsing statistics
|
|
if m.Type != nfs3Type && m.Type != nfs4Type {
|
|
return nil, fmt.Errorf("cannot parse MountStats for fstype %q", m.Type)
|
|
}
|
|
|
|
statVersion := strings.TrimPrefix(ss[8], statVersionPrefix)
|
|
|
|
stats, err := parseMountStatsNFS(s, statVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.Stats = stats
|
|
}
|
|
|
|
mounts = append(mounts, m)
|
|
}
|
|
|
|
return mounts, s.Err()
|
|
}
|
|
|
|
// parseMount parses an entry in /proc/[pid]/mountstats in the format:
|
|
// device [device] mounted on [mount] with fstype [type]
|
|
func parseMount(ss []string) (*Mount, error) {
|
|
if len(ss) < deviceEntryLen {
|
|
return nil, fmt.Errorf("invalid device entry: %v", ss)
|
|
}
|
|
|
|
// Check for specific words appearing at specific indices to ensure
|
|
// the format is consistent with what we expect
|
|
format := []struct {
|
|
i int
|
|
s string
|
|
}{
|
|
{i: 0, s: "device"},
|
|
{i: 2, s: "mounted"},
|
|
{i: 3, s: "on"},
|
|
{i: 5, s: "with"},
|
|
{i: 6, s: "fstype"},
|
|
}
|
|
|
|
for _, f := range format {
|
|
if ss[f.i] != f.s {
|
|
return nil, fmt.Errorf("invalid device entry: %v", ss)
|
|
}
|
|
}
|
|
|
|
return &Mount{
|
|
Device: ss[1],
|
|
Mount: ss[4],
|
|
Type: ss[7],
|
|
}, nil
|
|
}
|
|
|
|
// parseMountStatsNFS parses a MountStatsNFS by scanning additional information
|
|
// related to NFS statistics.
|
|
func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, error) {
|
|
// Field indicators for parsing specific types of data
|
|
const (
|
|
fieldOpts = "opts:"
|
|
fieldAge = "age:"
|
|
fieldBytes = "bytes:"
|
|
fieldEvents = "events:"
|
|
fieldPerOpStats = "per-op"
|
|
fieldTransport = "xprt:"
|
|
)
|
|
|
|
stats := &MountStatsNFS{
|
|
StatVersion: statVersion,
|
|
}
|
|
|
|
for s.Scan() {
|
|
ss := strings.Fields(string(s.Bytes()))
|
|
if len(ss) == 0 {
|
|
break
|
|
}
|
|
|
|
switch ss[0] {
|
|
case fieldOpts:
|
|
if len(ss) < 2 {
|
|
return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
|
|
}
|
|
if stats.Opts == nil {
|
|
stats.Opts = map[string]string{}
|
|
}
|
|
for _, opt := range strings.Split(ss[1], ",") {
|
|
split := strings.Split(opt, "=")
|
|
if len(split) == 2 {
|
|
stats.Opts[split[0]] = split[1]
|
|
} else {
|
|
stats.Opts[opt] = ""
|
|
}
|
|
}
|
|
case fieldAge:
|
|
if len(ss) < 2 {
|
|
return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
|
|
}
|
|
// Age integer is in seconds
|
|
d, err := time.ParseDuration(ss[1] + "s")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.Age = d
|
|
case fieldBytes:
|
|
if len(ss) < 2 {
|
|
return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
|
|
}
|
|
bstats, err := parseNFSBytesStats(ss[1:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.Bytes = *bstats
|
|
case fieldEvents:
|
|
if len(ss) < 2 {
|
|
return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
|
|
}
|
|
estats, err := parseNFSEventsStats(ss[1:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.Events = *estats
|
|
case fieldTransport:
|
|
if len(ss) < 3 {
|
|
return nil, fmt.Errorf("not enough information for NFS transport stats: %v", ss)
|
|
}
|
|
|
|
tstats, err := parseNFSTransportStats(ss[1:], statVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.Transport = *tstats
|
|
}
|
|
|
|
// When encountering "per-operation statistics", we must break this
|
|
// loop and parse them separately to ensure we can terminate parsing
|
|
// before reaching another device entry; hence why this 'if' statement
|
|
// is not just another switch case
|
|
if ss[0] == fieldPerOpStats {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := s.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// NFS per-operation stats appear last before the next device entry
|
|
perOpStats, err := parseNFSOperationStats(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.Operations = perOpStats
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// parseNFSBytesStats parses a NFSBytesStats line using an input set of
|
|
// integer fields.
|
|
func parseNFSBytesStats(ss []string) (*NFSBytesStats, error) {
|
|
if len(ss) != fieldBytesLen {
|
|
return nil, fmt.Errorf("invalid NFS bytes stats: %v", ss)
|
|
}
|
|
|
|
ns := make([]uint64, 0, fieldBytesLen)
|
|
for _, s := range ss {
|
|
n, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ns = append(ns, n)
|
|
}
|
|
|
|
return &NFSBytesStats{
|
|
Read: ns[0],
|
|
Write: ns[1],
|
|
DirectRead: ns[2],
|
|
DirectWrite: ns[3],
|
|
ReadTotal: ns[4],
|
|
WriteTotal: ns[5],
|
|
ReadPages: ns[6],
|
|
WritePages: ns[7],
|
|
}, nil
|
|
}
|
|
|
|
// parseNFSEventsStats parses a NFSEventsStats line using an input set of
|
|
// integer fields.
|
|
func parseNFSEventsStats(ss []string) (*NFSEventsStats, error) {
|
|
if len(ss) != fieldEventsLen {
|
|
return nil, fmt.Errorf("invalid NFS events stats: %v", ss)
|
|
}
|
|
|
|
ns := make([]uint64, 0, fieldEventsLen)
|
|
for _, s := range ss {
|
|
n, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ns = append(ns, n)
|
|
}
|
|
|
|
return &NFSEventsStats{
|
|
InodeRevalidate: ns[0],
|
|
DnodeRevalidate: ns[1],
|
|
DataInvalidate: ns[2],
|
|
AttributeInvalidate: ns[3],
|
|
VFSOpen: ns[4],
|
|
VFSLookup: ns[5],
|
|
VFSAccess: ns[6],
|
|
VFSUpdatePage: ns[7],
|
|
VFSReadPage: ns[8],
|
|
VFSReadPages: ns[9],
|
|
VFSWritePage: ns[10],
|
|
VFSWritePages: ns[11],
|
|
VFSGetdents: ns[12],
|
|
VFSSetattr: ns[13],
|
|
VFSFlush: ns[14],
|
|
VFSFsync: ns[15],
|
|
VFSLock: ns[16],
|
|
VFSFileRelease: ns[17],
|
|
CongestionWait: ns[18],
|
|
Truncation: ns[19],
|
|
WriteExtension: ns[20],
|
|
SillyRename: ns[21],
|
|
ShortRead: ns[22],
|
|
ShortWrite: ns[23],
|
|
JukeboxDelay: ns[24],
|
|
PNFSRead: ns[25],
|
|
PNFSWrite: ns[26],
|
|
}, nil
|
|
}
|
|
|
|
// parseNFSOperationStats parses a slice of NFSOperationStats by scanning
|
|
// additional information about per-operation statistics until an empty
|
|
// line is reached.
|
|
func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
|
|
const (
|
|
// Minimum number of expected fields in each per-operation statistics set
|
|
minFields = 9
|
|
)
|
|
|
|
var ops []NFSOperationStats
|
|
|
|
for s.Scan() {
|
|
ss := strings.Fields(string(s.Bytes()))
|
|
if len(ss) == 0 {
|
|
// Must break when reading a blank line after per-operation stats to
|
|
// enable top-level function to parse the next device entry
|
|
break
|
|
}
|
|
|
|
if len(ss) < minFields {
|
|
return nil, fmt.Errorf("invalid NFS per-operations stats: %v", ss)
|
|
}
|
|
|
|
// Skip string operation name for integers
|
|
ns := make([]uint64, 0, minFields-1)
|
|
for _, st := range ss[1:] {
|
|
n, err := strconv.ParseUint(st, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ns = append(ns, n)
|
|
}
|
|
|
|
opStats := NFSOperationStats{
|
|
Operation: strings.TrimSuffix(ss[0], ":"),
|
|
Requests: ns[0],
|
|
Transmissions: ns[1],
|
|
MajorTimeouts: ns[2],
|
|
BytesSent: ns[3],
|
|
BytesReceived: ns[4],
|
|
CumulativeQueueMilliseconds: ns[5],
|
|
CumulativeTotalResponseMilliseconds: ns[6],
|
|
CumulativeTotalRequestMilliseconds: ns[7],
|
|
}
|
|
|
|
if len(ns) > 8 {
|
|
opStats.Errors = ns[8]
|
|
}
|
|
|
|
ops = append(ops, opStats)
|
|
}
|
|
|
|
return ops, s.Err()
|
|
}
|
|
|
|
// parseNFSTransportStats parses a NFSTransportStats line using an input set of
|
|
// integer fields matched to a specific stats version.
|
|
func parseNFSTransportStats(ss []string, statVersion string) (*NFSTransportStats, error) {
|
|
// Extract the protocol field. It is the only string value in the line
|
|
protocol := ss[0]
|
|
ss = ss[1:]
|
|
|
|
switch statVersion {
|
|
case statVersion10:
|
|
var expectedLength int
|
|
if protocol == "tcp" {
|
|
expectedLength = fieldTransport10TCPLen
|
|
} else if protocol == "udp" {
|
|
expectedLength = fieldTransport10UDPLen
|
|
} else {
|
|
return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.0 statement: %v", protocol, ss)
|
|
}
|
|
if len(ss) != expectedLength {
|
|
return nil, fmt.Errorf("invalid NFS transport stats 1.0 statement: %v", ss)
|
|
}
|
|
case statVersion11:
|
|
var expectedLength int
|
|
if protocol == "tcp" {
|
|
expectedLength = fieldTransport11TCPLen
|
|
} else if protocol == "udp" {
|
|
expectedLength = fieldTransport11UDPLen
|
|
} else {
|
|
return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.1 statement: %v", protocol, ss)
|
|
}
|
|
if len(ss) != expectedLength {
|
|
return nil, fmt.Errorf("invalid NFS transport stats 1.1 statement: %v", ss)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unrecognized NFS transport stats version: %q", statVersion)
|
|
}
|
|
|
|
// Allocate enough for v1.1 stats since zero value for v1.1 stats will be okay
|
|
// in a v1.0 response. Since the stat length is bigger for TCP stats, we use
|
|
// the TCP length here.
|
|
//
|
|
// Note: slice length must be set to length of v1.1 stats to avoid a panic when
|
|
// only v1.0 stats are present.
|
|
// See: https://github.com/prometheus/node_exporter/issues/571.
|
|
ns := make([]uint64, fieldTransport11TCPLen)
|
|
for i, s := range ss {
|
|
n, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ns[i] = n
|
|
}
|
|
|
|
// The fields differ depending on the transport protocol (TCP or UDP)
|
|
// From https://utcc.utoronto.ca/%7Ecks/space/blog/linux/NFSMountstatsXprt
|
|
//
|
|
// For the udp RPC transport there is no connection count, connect idle time,
|
|
// or idle time (fields #3, #4, and #5); all other fields are the same. So
|
|
// we set them to 0 here.
|
|
if protocol == "udp" {
|
|
ns = append(ns[:2], append(make([]uint64, 3), ns[2:]...)...)
|
|
}
|
|
|
|
return &NFSTransportStats{
|
|
Protocol: protocol,
|
|
Port: ns[0],
|
|
Bind: ns[1],
|
|
Connect: ns[2],
|
|
ConnectIdleTime: ns[3],
|
|
IdleTimeSeconds: ns[4],
|
|
Sends: ns[5],
|
|
Receives: ns[6],
|
|
BadTransactionIDs: ns[7],
|
|
CumulativeActiveRequests: ns[8],
|
|
CumulativeBacklog: ns[9],
|
|
MaximumRPCSlotsUsed: ns[10],
|
|
CumulativeSendingQueue: ns[11],
|
|
CumulativePendingQueue: ns[12],
|
|
}, nil
|
|
}
|