misc: bug fixes and improvements for stats Fuse fs
[vpp.git] / extras / vpp_stats_fs / stats_fs.go
1 /*
2  * Copyright (c) 2021 Cisco Systems and/or its affiliates.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at:
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15
16 /*Go-FUSE allows us to define the behaviour of our filesystem by recoding any primitive function we need.
17  *The structure of the filesystem is constructed as a tree.
18  *Each type of nodes (root, directory, file) follows its own prmitives.
19  */
20 package main
21
22 import (
23         "context"
24         "fmt"
25         "path"
26         "path/filepath"
27         "strings"
28         "syscall"
29         "time"
30
31         "github.com/hanwen/go-fuse/v2/fs"
32         "github.com/hanwen/go-fuse/v2/fuse"
33
34         "git.fd.io/govpp.git/adapter"
35         "git.fd.io/govpp.git/adapter/statsclient"
36 )
37
38 func updateDir(ctx context.Context, n *fs.Inode, cl *statsclient.StatsClient, dirPath string) syscall.Errno {
39         stats, err := cl.PrepareDir(dirPath)
40         if err != nil {
41                 LogMsg(fmt.Sprintf("Listing stats index failed: %v\n", err))
42                 return syscall.EAGAIN
43         }
44
45         n.Operations().(*dirNode).epoch = stats.Epoch
46
47         n.RmAllChildren()
48
49         for _, entry := range stats.Entries {
50                 localPath := strings.TrimPrefix(string(entry.Name), dirPath)
51                 dirPath, base := filepath.Split(localPath)
52
53                 parent := n
54                 for _, component := range strings.Split(dirPath, "/") {
55                         if len(component) == 0 {
56                                 continue
57                         }
58                         child := parent.GetChild(component)
59                         if child == nil {
60                                 child = parent.NewInode(ctx, &dirNode{client: cl, epoch: stats.Epoch},
61                                         fs.StableAttr{Mode: fuse.S_IFDIR})
62                                 parent.AddChild(component, child, true)
63                         } else {
64                                 child.Operations().(*dirNode).epoch = stats.Epoch
65                         }
66
67                         parent = child
68                 }
69
70                 filename := strings.Replace(base, " ", "_", -1)
71                 child := parent.GetChild(filename)
72                 if child == nil {
73                         child := parent.NewPersistentInode(ctx, &statNode{client: cl, index: entry.Index}, fs.StableAttr{})
74                         parent.AddChild(filename, child, true)
75                 }
76         }
77         return 0
78 }
79
80 func getCounterContent(index uint32, client *statsclient.StatsClient) (content string, status syscall.Errno) {
81         content = ""
82         statsDir, err := client.PrepareDirOnIndex(index)
83         if err != nil {
84                 LogMsg(fmt.Sprintf("Dumping stats on index failed: %v\n", err))
85                 return content, syscall.EAGAIN
86         }
87         if len(statsDir.Entries) != 1 {
88                 return content, syscall.ENOENT
89         }
90         result := statsDir.Entries[0]
91         if result.Data == nil {
92                 return content, 0
93         }
94
95         switch result.Type {
96         case adapter.ScalarIndex:
97                 stats := result.Data.(adapter.ScalarStat)
98                 content = fmt.Sprintf("%.2f\n", stats)
99         case adapter.ErrorIndex:
100                 stats := result.Data.(adapter.ErrorStat)
101                 content = fmt.Sprintf("%-16s%s\n", "Index", "Count")
102                 for i, value := range stats {
103                         content += fmt.Sprintf("%-16d%d\n", i, value)
104                 }
105         case adapter.SimpleCounterVector:
106                 stats := result.Data.(adapter.SimpleCounterStat)
107                 content = fmt.Sprintf("%-16s%-16s%s\n", "Thread", "Index", "Packets")
108                 for i, vector := range stats {
109                         for j, value := range vector {
110                                 content += fmt.Sprintf("%-16d%-16d%d\n", i, j, value)
111                         }
112                 }
113         case adapter.CombinedCounterVector:
114                 stats := result.Data.(adapter.CombinedCounterStat)
115                 content = fmt.Sprintf("%-16s%-16s%-16s%s\n", "Thread", "Index", "Packets", "Bytes")
116                 for i, vector := range stats {
117                         for j, value := range vector {
118                                 content += fmt.Sprintf("%-16d%-16d%-16d%d\n", i, j, value[0], value[1])
119                         }
120                 }
121         case adapter.NameVector:
122                 stats := result.Data.(adapter.NameStat)
123                 content = fmt.Sprintf("%-16s%s\n", "Index", "Name")
124                 for i, value := range stats {
125                         content += fmt.Sprintf("%-16d%s\n", i, string(value))
126                 }
127         default:
128                 content = fmt.Sprintf("Unknown stat type: %d\n", result.Type)
129                 //For now, the empty type (file deleted) is not implemented in GoVPP
130                 return content, syscall.ENOENT
131         }
132         return content, fs.OK
133 }
134
135 //The dirNode structure represents directories
136 type dirNode struct {
137         fs.Inode
138         client *statsclient.StatsClient
139         epoch  int64
140 }
141
142 var _ = (fs.NodeOpendirer)((*dirNode)(nil))
143 var _ = (fs.NodeGetattrer)((*dirNode)(nil))
144 var _ = (fs.NodeOnAdder)((*dirNode)(nil))
145
146 func (dn *dirNode) OnAdd(ctx context.Context) {
147         if dn.Inode.IsRoot() {
148                 updateDir(ctx, &dn.Inode, dn.client, "/")
149         }
150 }
151
152 func (dn *dirNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
153         out.Mtime = uint64(time.Now().Unix())
154         out.Atime = out.Mtime
155         out.Ctime = out.Mtime
156         return 0
157 }
158
159 func (dn *dirNode) Opendir(ctx context.Context) syscall.Errno {
160         var status syscall.Errno = syscall.F_OK
161         var sleepTime time.Duration = 10 * time.Millisecond
162         newEpoch, inProgress := dn.client.GetEpoch()
163         for inProgress {
164                 newEpoch, inProgress = dn.client.GetEpoch()
165                 time.Sleep(sleepTime)
166                 sleepTime = sleepTime * 2
167         }
168
169         //We check that the directory epoch is up to date
170         if dn.epoch != newEpoch {
171                 //directoryPath is the path to the current directory from root
172                 directoryPath := path.Clean("/" + dn.Inode.Path(nil) + "/")
173                 status = updateDir(ctx, &dn.Inode, dn.client, directoryPath)
174         }
175         return status
176 }
177
178 //The statNode structure represents counters
179 type statNode struct {
180         fs.Inode
181         client *statsclient.StatsClient
182         index  uint32
183 }
184
185 var _ = (fs.NodeOpener)((*statNode)(nil))
186 var _ = (fs.NodeGetattrer)((*statNode)(nil))
187
188 func (fh *statNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
189         out.Mtime = uint64(time.Now().Unix())
190         out.Atime = out.Mtime
191         out.Ctime = out.Mtime
192         return 0
193 }
194
195 //When a file is opened, the correpsonding counter value is dumped and a file handle is created
196 func (sn *statNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
197         content, status := getCounterContent(sn.index, sn.client)
198         if status == syscall.ENOENT {
199                 _, parent := sn.Inode.Parent()
200                 parent.RmChild(sn.Inode.Path(parent))
201
202         }
203         return &statFH{data: []byte(content)}, fuse.FOPEN_DIRECT_IO, status
204 }
205
206 /* The statFH structure aims at dislaying the counters dynamically.
207  * It allows the Kernel to read data as I/O without having to specify files sizes, as they may evolve dynamically.
208  */
209 type statFH struct {
210         data []byte
211 }
212
213 var _ = (fs.FileReader)((*statFH)(nil))
214
215 func (fh *statFH) Read(ctx context.Context, data []byte, off int64) (fuse.ReadResult, syscall.Errno) {
216         end := int(off) + len(data)
217         if end > len(fh.data) {
218                 end = len(fh.data)
219         }
220         return fuse.ReadResultData(fh.data[off:end]), fs.OK
221 }
222
223 //NewStatsFileSystem creates the fs for the stat segment.
224 func NewStatsFileSystem(sc *statsclient.StatsClient) (root fs.InodeEmbedder, err error) {
225         return &dirNode{client: sc}, nil
226 }