blob: 35b2ef3513f915f89f9a09764ff92a6c586e2088 [file] [log] [blame]
kesavand2cde6582020-06-22 04:56:23 -04001// Copyright 2018 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package procfs
15
16// While implementing parsing of /proc/[pid]/mountstats, this blog was used
17// heavily as a reference:
18// https://utcc.utoronto.ca/~cks/space/blog/linux/NFSMountstatsIndex
19//
20// Special thanks to Chris Siebenmann for all of his posts explaining the
21// various statistics available for NFS.
22
23import (
24 "bufio"
25 "fmt"
26 "io"
27 "strconv"
28 "strings"
29 "time"
30)
31
32// Constants shared between multiple functions.
33const (
34 deviceEntryLen = 8
35
36 fieldBytesLen = 8
37 fieldEventsLen = 27
38
39 statVersion10 = "1.0"
40 statVersion11 = "1.1"
41
42 fieldTransport10TCPLen = 10
43 fieldTransport10UDPLen = 7
44
45 fieldTransport11TCPLen = 13
46 fieldTransport11UDPLen = 10
47)
48
49// A Mount is a device mount parsed from /proc/[pid]/mountstats.
50type Mount struct {
51 // Name of the device.
52 Device string
53 // The mount point of the device.
54 Mount string
55 // The filesystem type used by the device.
56 Type string
57 // If available additional statistics related to this Mount.
58 // Use a type assertion to determine if additional statistics are available.
59 Stats MountStats
60}
61
62// A MountStats is a type which contains detailed statistics for a specific
63// type of Mount.
64type MountStats interface {
65 mountStats()
66}
67
68// A MountStatsNFS is a MountStats implementation for NFSv3 and v4 mounts.
69type MountStatsNFS struct {
70 // The version of statistics provided.
71 StatVersion string
72 // The mount options of the NFS mount.
73 Opts map[string]string
74 // The age of the NFS mount.
75 Age time.Duration
76 // Statistics related to byte counters for various operations.
77 Bytes NFSBytesStats
78 // Statistics related to various NFS event occurrences.
79 Events NFSEventsStats
80 // Statistics broken down by filesystem operation.
81 Operations []NFSOperationStats
82 // Statistics about the NFS RPC transport.
83 Transport NFSTransportStats
84}
85
86// mountStats implements MountStats.
87func (m MountStatsNFS) mountStats() {}
88
89// A NFSBytesStats contains statistics about the number of bytes read and written
90// by an NFS client to and from an NFS server.
91type NFSBytesStats struct {
92 // Number of bytes read using the read() syscall.
93 Read uint64
94 // Number of bytes written using the write() syscall.
95 Write uint64
96 // Number of bytes read using the read() syscall in O_DIRECT mode.
97 DirectRead uint64
98 // Number of bytes written using the write() syscall in O_DIRECT mode.
99 DirectWrite uint64
100 // Number of bytes read from the NFS server, in total.
101 ReadTotal uint64
102 // Number of bytes written to the NFS server, in total.
103 WriteTotal uint64
104 // Number of pages read directly via mmap()'d files.
105 ReadPages uint64
106 // Number of pages written directly via mmap()'d files.
107 WritePages uint64
108}
109
110// A NFSEventsStats contains statistics about NFS event occurrences.
111type NFSEventsStats struct {
112 // Number of times cached inode attributes are re-validated from the server.
113 InodeRevalidate uint64
114 // Number of times cached dentry nodes are re-validated from the server.
115 DnodeRevalidate uint64
116 // Number of times an inode cache is cleared.
117 DataInvalidate uint64
118 // Number of times cached inode attributes are invalidated.
119 AttributeInvalidate uint64
120 // Number of times files or directories have been open()'d.
121 VFSOpen uint64
122 // Number of times a directory lookup has occurred.
123 VFSLookup uint64
124 // Number of times permissions have been checked.
125 VFSAccess uint64
126 // Number of updates (and potential writes) to pages.
127 VFSUpdatePage uint64
128 // Number of pages read directly via mmap()'d files.
129 VFSReadPage uint64
130 // Number of times a group of pages have been read.
131 VFSReadPages uint64
132 // Number of pages written directly via mmap()'d files.
133 VFSWritePage uint64
134 // Number of times a group of pages have been written.
135 VFSWritePages uint64
136 // Number of times directory entries have been read with getdents().
137 VFSGetdents uint64
138 // Number of times attributes have been set on inodes.
139 VFSSetattr uint64
140 // Number of pending writes that have been forcefully flushed to the server.
141 VFSFlush uint64
142 // Number of times fsync() has been called on directories and files.
143 VFSFsync uint64
144 // Number of times locking has been attempted on a file.
145 VFSLock uint64
146 // Number of times files have been closed and released.
147 VFSFileRelease uint64
148 // Unknown. Possibly unused.
149 CongestionWait uint64
150 // Number of times files have been truncated.
151 Truncation uint64
152 // Number of times a file has been grown due to writes beyond its existing end.
153 WriteExtension uint64
154 // Number of times a file was removed while still open by another process.
155 SillyRename uint64
156 // Number of times the NFS server gave less data than expected while reading.
157 ShortRead uint64
158 // Number of times the NFS server wrote less data than expected while writing.
159 ShortWrite uint64
160 // Number of times the NFS server indicated EJUKEBOX; retrieving data from
161 // offline storage.
162 JukeboxDelay uint64
163 // Number of NFS v4.1+ pNFS reads.
164 PNFSRead uint64
165 // Number of NFS v4.1+ pNFS writes.
166 PNFSWrite uint64
167}
168
169// A NFSOperationStats contains statistics for a single operation.
170type NFSOperationStats struct {
171 // The name of the operation.
172 Operation string
173 // Number of requests performed for this operation.
174 Requests uint64
175 // Number of times an actual RPC request has been transmitted for this operation.
176 Transmissions uint64
177 // Number of times a request has had a major timeout.
178 MajorTimeouts uint64
179 // Number of bytes sent for this operation, including RPC headers and payload.
180 BytesSent uint64
181 // Number of bytes received for this operation, including RPC headers and payload.
182 BytesReceived uint64
183 // Duration all requests spent queued for transmission before they were sent.
184 CumulativeQueueMilliseconds uint64
185 // Duration it took to get a reply back after the request was transmitted.
186 CumulativeTotalResponseMilliseconds uint64
187 // Duration from when a request was enqueued to when it was completely handled.
188 CumulativeTotalRequestMilliseconds uint64
189}
190
191// A NFSTransportStats contains statistics for the NFS mount RPC requests and
192// responses.
193type NFSTransportStats struct {
194 // The transport protocol used for the NFS mount.
195 Protocol string
196 // The local port used for the NFS mount.
197 Port uint64
198 // Number of times the client has had to establish a connection from scratch
199 // to the NFS server.
200 Bind uint64
201 // Number of times the client has made a TCP connection to the NFS server.
202 Connect uint64
203 // Duration (in jiffies, a kernel internal unit of time) the NFS mount has
204 // spent waiting for connections to the server to be established.
205 ConnectIdleTime uint64
206 // Duration since the NFS mount last saw any RPC traffic.
207 IdleTimeSeconds uint64
208 // Number of RPC requests for this mount sent to the NFS server.
209 Sends uint64
210 // Number of RPC responses for this mount received from the NFS server.
211 Receives uint64
212 // Number of times the NFS server sent a response with a transaction ID
213 // unknown to this client.
214 BadTransactionIDs uint64
215 // A running counter, incremented on each request as the current difference
216 // ebetween sends and receives.
217 CumulativeActiveRequests uint64
218 // A running counter, incremented on each request by the current backlog
219 // queue size.
220 CumulativeBacklog uint64
221
222 // Stats below only available with stat version 1.1.
223
224 // Maximum number of simultaneously active RPC requests ever used.
225 MaximumRPCSlotsUsed uint64
226 // A running counter, incremented on each request as the current size of the
227 // sending queue.
228 CumulativeSendingQueue uint64
229 // A running counter, incremented on each request as the current size of the
230 // pending queue.
231 CumulativePendingQueue uint64
232}
233
234// parseMountStats parses a /proc/[pid]/mountstats file and returns a slice
235// of Mount structures containing detailed information about each mount.
236// If available, statistics for each mount are parsed as well.
237func parseMountStats(r io.Reader) ([]*Mount, error) {
238 const (
239 device = "device"
240 statVersionPrefix = "statvers="
241
242 nfs3Type = "nfs"
243 nfs4Type = "nfs4"
244 )
245
246 var mounts []*Mount
247
248 s := bufio.NewScanner(r)
249 for s.Scan() {
250 // Only look for device entries in this function
251 ss := strings.Fields(string(s.Bytes()))
252 if len(ss) == 0 || ss[0] != device {
253 continue
254 }
255
256 m, err := parseMount(ss)
257 if err != nil {
258 return nil, err
259 }
260
261 // Does this mount also possess statistics information?
262 if len(ss) > deviceEntryLen {
263 // Only NFSv3 and v4 are supported for parsing statistics
264 if m.Type != nfs3Type && m.Type != nfs4Type {
265 return nil, fmt.Errorf("cannot parse MountStats for fstype %q", m.Type)
266 }
267
268 statVersion := strings.TrimPrefix(ss[8], statVersionPrefix)
269
270 stats, err := parseMountStatsNFS(s, statVersion)
271 if err != nil {
272 return nil, err
273 }
274
275 m.Stats = stats
276 }
277
278 mounts = append(mounts, m)
279 }
280
281 return mounts, s.Err()
282}
283
284// parseMount parses an entry in /proc/[pid]/mountstats in the format:
285// device [device] mounted on [mount] with fstype [type]
286func parseMount(ss []string) (*Mount, error) {
287 if len(ss) < deviceEntryLen {
288 return nil, fmt.Errorf("invalid device entry: %v", ss)
289 }
290
291 // Check for specific words appearing at specific indices to ensure
292 // the format is consistent with what we expect
293 format := []struct {
294 i int
295 s string
296 }{
297 {i: 0, s: "device"},
298 {i: 2, s: "mounted"},
299 {i: 3, s: "on"},
300 {i: 5, s: "with"},
301 {i: 6, s: "fstype"},
302 }
303
304 for _, f := range format {
305 if ss[f.i] != f.s {
306 return nil, fmt.Errorf("invalid device entry: %v", ss)
307 }
308 }
309
310 return &Mount{
311 Device: ss[1],
312 Mount: ss[4],
313 Type: ss[7],
314 }, nil
315}
316
317// parseMountStatsNFS parses a MountStatsNFS by scanning additional information
318// related to NFS statistics.
319func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, error) {
320 // Field indicators for parsing specific types of data
321 const (
322 fieldOpts = "opts:"
323 fieldAge = "age:"
324 fieldBytes = "bytes:"
325 fieldEvents = "events:"
326 fieldPerOpStats = "per-op"
327 fieldTransport = "xprt:"
328 )
329
330 stats := &MountStatsNFS{
331 StatVersion: statVersion,
332 }
333
334 for s.Scan() {
335 ss := strings.Fields(string(s.Bytes()))
336 if len(ss) == 0 {
337 break
338 }
339 if len(ss) < 2 {
340 return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
341 }
342
343 switch ss[0] {
344 case fieldOpts:
345 if stats.Opts == nil {
346 stats.Opts = map[string]string{}
347 }
348 for _, opt := range strings.Split(ss[1], ",") {
349 split := strings.Split(opt, "=")
350 if len(split) == 2 {
351 stats.Opts[split[0]] = split[1]
352 } else {
353 stats.Opts[opt] = ""
354 }
355 }
356 case fieldAge:
357 // Age integer is in seconds
358 d, err := time.ParseDuration(ss[1] + "s")
359 if err != nil {
360 return nil, err
361 }
362
363 stats.Age = d
364 case fieldBytes:
365 bstats, err := parseNFSBytesStats(ss[1:])
366 if err != nil {
367 return nil, err
368 }
369
370 stats.Bytes = *bstats
371 case fieldEvents:
372 estats, err := parseNFSEventsStats(ss[1:])
373 if err != nil {
374 return nil, err
375 }
376
377 stats.Events = *estats
378 case fieldTransport:
379 if len(ss) < 3 {
380 return nil, fmt.Errorf("not enough information for NFS transport stats: %v", ss)
381 }
382
383 tstats, err := parseNFSTransportStats(ss[1:], statVersion)
384 if err != nil {
385 return nil, err
386 }
387
388 stats.Transport = *tstats
389 }
390
391 // When encountering "per-operation statistics", we must break this
392 // loop and parse them separately to ensure we can terminate parsing
393 // before reaching another device entry; hence why this 'if' statement
394 // is not just another switch case
395 if ss[0] == fieldPerOpStats {
396 break
397 }
398 }
399
400 if err := s.Err(); err != nil {
401 return nil, err
402 }
403
404 // NFS per-operation stats appear last before the next device entry
405 perOpStats, err := parseNFSOperationStats(s)
406 if err != nil {
407 return nil, err
408 }
409
410 stats.Operations = perOpStats
411
412 return stats, nil
413}
414
415// parseNFSBytesStats parses a NFSBytesStats line using an input set of
416// integer fields.
417func parseNFSBytesStats(ss []string) (*NFSBytesStats, error) {
418 if len(ss) != fieldBytesLen {
419 return nil, fmt.Errorf("invalid NFS bytes stats: %v", ss)
420 }
421
422 ns := make([]uint64, 0, fieldBytesLen)
423 for _, s := range ss {
424 n, err := strconv.ParseUint(s, 10, 64)
425 if err != nil {
426 return nil, err
427 }
428
429 ns = append(ns, n)
430 }
431
432 return &NFSBytesStats{
433 Read: ns[0],
434 Write: ns[1],
435 DirectRead: ns[2],
436 DirectWrite: ns[3],
437 ReadTotal: ns[4],
438 WriteTotal: ns[5],
439 ReadPages: ns[6],
440 WritePages: ns[7],
441 }, nil
442}
443
444// parseNFSEventsStats parses a NFSEventsStats line using an input set of
445// integer fields.
446func parseNFSEventsStats(ss []string) (*NFSEventsStats, error) {
447 if len(ss) != fieldEventsLen {
448 return nil, fmt.Errorf("invalid NFS events stats: %v", ss)
449 }
450
451 ns := make([]uint64, 0, fieldEventsLen)
452 for _, s := range ss {
453 n, err := strconv.ParseUint(s, 10, 64)
454 if err != nil {
455 return nil, err
456 }
457
458 ns = append(ns, n)
459 }
460
461 return &NFSEventsStats{
462 InodeRevalidate: ns[0],
463 DnodeRevalidate: ns[1],
464 DataInvalidate: ns[2],
465 AttributeInvalidate: ns[3],
466 VFSOpen: ns[4],
467 VFSLookup: ns[5],
468 VFSAccess: ns[6],
469 VFSUpdatePage: ns[7],
470 VFSReadPage: ns[8],
471 VFSReadPages: ns[9],
472 VFSWritePage: ns[10],
473 VFSWritePages: ns[11],
474 VFSGetdents: ns[12],
475 VFSSetattr: ns[13],
476 VFSFlush: ns[14],
477 VFSFsync: ns[15],
478 VFSLock: ns[16],
479 VFSFileRelease: ns[17],
480 CongestionWait: ns[18],
481 Truncation: ns[19],
482 WriteExtension: ns[20],
483 SillyRename: ns[21],
484 ShortRead: ns[22],
485 ShortWrite: ns[23],
486 JukeboxDelay: ns[24],
487 PNFSRead: ns[25],
488 PNFSWrite: ns[26],
489 }, nil
490}
491
492// parseNFSOperationStats parses a slice of NFSOperationStats by scanning
493// additional information about per-operation statistics until an empty
494// line is reached.
495func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
496 const (
497 // Number of expected fields in each per-operation statistics set
498 numFields = 9
499 )
500
501 var ops []NFSOperationStats
502
503 for s.Scan() {
504 ss := strings.Fields(string(s.Bytes()))
505 if len(ss) == 0 {
506 // Must break when reading a blank line after per-operation stats to
507 // enable top-level function to parse the next device entry
508 break
509 }
510
511 if len(ss) != numFields {
512 return nil, fmt.Errorf("invalid NFS per-operations stats: %v", ss)
513 }
514
515 // Skip string operation name for integers
516 ns := make([]uint64, 0, numFields-1)
517 for _, st := range ss[1:] {
518 n, err := strconv.ParseUint(st, 10, 64)
519 if err != nil {
520 return nil, err
521 }
522
523 ns = append(ns, n)
524 }
525
526 ops = append(ops, NFSOperationStats{
527 Operation: strings.TrimSuffix(ss[0], ":"),
528 Requests: ns[0],
529 Transmissions: ns[1],
530 MajorTimeouts: ns[2],
531 BytesSent: ns[3],
532 BytesReceived: ns[4],
533 CumulativeQueueMilliseconds: ns[5],
534 CumulativeTotalResponseMilliseconds: ns[6],
535 CumulativeTotalRequestMilliseconds: ns[7],
536 })
537 }
538
539 return ops, s.Err()
540}
541
542// parseNFSTransportStats parses a NFSTransportStats line using an input set of
543// integer fields matched to a specific stats version.
544func parseNFSTransportStats(ss []string, statVersion string) (*NFSTransportStats, error) {
545 // Extract the protocol field. It is the only string value in the line
546 protocol := ss[0]
547 ss = ss[1:]
548
549 switch statVersion {
550 case statVersion10:
551 var expectedLength int
552 if protocol == "tcp" {
553 expectedLength = fieldTransport10TCPLen
554 } else if protocol == "udp" {
555 expectedLength = fieldTransport10UDPLen
556 } else {
557 return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.0 statement: %v", protocol, ss)
558 }
559 if len(ss) != expectedLength {
560 return nil, fmt.Errorf("invalid NFS transport stats 1.0 statement: %v", ss)
561 }
562 case statVersion11:
563 var expectedLength int
564 if protocol == "tcp" {
565 expectedLength = fieldTransport11TCPLen
566 } else if protocol == "udp" {
567 expectedLength = fieldTransport11UDPLen
568 } else {
569 return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.1 statement: %v", protocol, ss)
570 }
571 if len(ss) != expectedLength {
572 return nil, fmt.Errorf("invalid NFS transport stats 1.1 statement: %v", ss)
573 }
574 default:
575 return nil, fmt.Errorf("unrecognized NFS transport stats version: %q", statVersion)
576 }
577
578 // Allocate enough for v1.1 stats since zero value for v1.1 stats will be okay
579 // in a v1.0 response. Since the stat length is bigger for TCP stats, we use
580 // the TCP length here.
581 //
582 // Note: slice length must be set to length of v1.1 stats to avoid a panic when
583 // only v1.0 stats are present.
584 // See: https://github.com/prometheus/node_exporter/issues/571.
585 ns := make([]uint64, fieldTransport11TCPLen)
586 for i, s := range ss {
587 n, err := strconv.ParseUint(s, 10, 64)
588 if err != nil {
589 return nil, err
590 }
591
592 ns[i] = n
593 }
594
595 // The fields differ depending on the transport protocol (TCP or UDP)
596 // From https://utcc.utoronto.ca/%7Ecks/space/blog/linux/NFSMountstatsXprt
597 //
598 // For the udp RPC transport there is no connection count, connect idle time,
599 // or idle time (fields #3, #4, and #5); all other fields are the same. So
600 // we set them to 0 here.
601 if protocol == "udp" {
602 ns = append(ns[:2], append(make([]uint64, 3), ns[2:]...)...)
603 }
604
605 return &NFSTransportStats{
606 Protocol: protocol,
607 Port: ns[0],
608 Bind: ns[1],
609 Connect: ns[2],
610 ConnectIdleTime: ns[3],
611 IdleTimeSeconds: ns[4],
612 Sends: ns[5],
613 Receives: ns[6],
614 BadTransactionIDs: ns[7],
615 CumulativeActiveRequests: ns[8],
616 CumulativeBacklog: ns[9],
617 MaximumRPCSlotsUsed: ns[10],
618 CumulativeSendingQueue: ns[11],
619 CumulativePendingQueue: ns[12],
620 }, nil
621}