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