misc: fuse fs for the stats segment
[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         "log"
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         list, err := cl.ListStats(dirPath)
40         if err != nil {
41                 log.Println("list stats failed:", err)
42                 return syscall.EAGAIN
43         }
44
45         if list == nil {
46                 n.ForgetPersistent()
47                 return syscall.ENOENT
48         }
49
50         for _, path := range list {
51                 localPath := strings.TrimPrefix(path, dirPath)
52                 dir, base := filepath.Split(localPath)
53
54                 parent := n
55                 for _, component := range strings.Split(dir, "/") {
56                         if len(component) == 0 {
57                                 continue
58                         }
59                         child := parent.GetChild(component)
60                         if child == nil {
61                                 child = parent.NewPersistentInode(ctx, &dirNode{client: cl, lastUpdate: time.Now()},
62                                         fs.StableAttr{Mode: fuse.S_IFDIR})
63                                 parent.AddChild(component, child, true)
64                         }
65
66                         parent = child
67                 }
68                 filename := strings.Replace(base, " ", "_", -1)
69                 child := parent.GetChild(filename)
70                 if child == nil {
71                         child := parent.NewPersistentInode(ctx, &statNode{client: cl, path: path}, fs.StableAttr{})
72                         parent.AddChild(filename, child, true)
73                 }
74         }
75         return 0
76 }
77
78 func getCounterContent(path string, client *statsclient.StatsClient) (content string, status syscall.Errno) {
79         content = ""
80         //We add '$' because we deal with regexp here
81         res, err := client.DumpStats(path + "$")
82         if err != nil {
83                 return content, syscall.EAGAIN
84         }
85         if res == nil {
86                 return content, syscall.ENOENT
87         }
88
89         result := res[0]
90         if result.Data == nil {
91                 return content, 0
92         }
93
94         switch result.Type {
95         case adapter.ScalarIndex:
96                 stats := result.Data.(adapter.ScalarStat)
97                 content = fmt.Sprintf("%.2f\n", stats)
98         case adapter.ErrorIndex:
99                 stats := result.Data.(adapter.ErrorStat)
100                 content = fmt.Sprintf("%-16s%s\n", "Index", "Count")
101                 for i, value := range stats {
102                         content += fmt.Sprintf("%-16d%d\n", i, value)
103                 }
104         case adapter.SimpleCounterVector:
105                 stats := result.Data.(adapter.SimpleCounterStat)
106                 content = fmt.Sprintf("%-16s%-16s%s\n", "Thread", "Index", "Packets")
107                 for i, vector := range stats {
108                         for j, value := range vector {
109                                 content += fmt.Sprintf("%-16d%-16d%d\n", i, j, value)
110                         }
111                 }
112         case adapter.CombinedCounterVector:
113                 stats := result.Data.(adapter.CombinedCounterStat)
114                 content = fmt.Sprintf("%-16s%-16s%-16s%s\n", "Thread", "Index", "Packets", "Bytes")
115                 for i, vector := range stats {
116                         for j, value := range vector {
117                                 content += fmt.Sprintf("%-16d%-16d%-16d%d\n", i, j, value[0], value[1])
118                         }
119                 }
120         case adapter.NameVector:
121                 stats := result.Data.(adapter.NameStat)
122                 content = fmt.Sprintf("%-16s%s\n", "Index", "Name")
123                 for i, value := range stats {
124                         content += fmt.Sprintf("%-16d%s\n", i, string(value))
125                 }
126         default:
127                 content = fmt.Sprintf("Unknown stat type: %d\n", result.Type)
128                 //For now, the empty type (file deleted) is not implemented in GoVPP
129                 return content, syscall.ENOENT
130         }
131         return content, fs.OK
132 }
133
134 type rootNode struct {
135         fs.Inode
136         client     *statsclient.StatsClient
137         lastUpdate time.Time
138 }
139
140 var _ = (fs.NodeOnAdder)((*rootNode)(nil))
141
142 func (root *rootNode) OnAdd(ctx context.Context) {
143         updateDir(ctx, &root.Inode, root.client, "/")
144         root.lastUpdate = time.Now()
145 }
146
147 //The dirNode structure represents directories
148 type dirNode struct {
149         fs.Inode
150         client     *statsclient.StatsClient
151         lastUpdate time.Time
152 }
153
154 var _ = (fs.NodeOpendirer)((*dirNode)(nil))
155
156 func (dn *dirNode) Opendir(ctx context.Context) syscall.Errno {
157         //We do not update a directory more than once a second, as counters are rarely added/deleted.
158         if time.Now().Sub(dn.lastUpdate) < time.Second {
159                 return 0
160         }
161
162         //directoryPath is the path to the current directory from root
163         directoryPath := "/" + dn.Inode.Path(nil) + "/"
164         status := updateDir(ctx, &dn.Inode, dn.client, directoryPath)
165         dn.lastUpdate = time.Now()
166         return status
167 }
168
169 //The statNode structure represents counters
170 type statNode struct {
171         fs.Inode
172         client *statsclient.StatsClient
173         path   string
174 }
175
176 var _ = (fs.NodeOpener)((*statNode)(nil))
177
178 //When a file is opened, the correpsonding counter value is dumped and a file handle is created
179 func (sn *statNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
180         content, status := getCounterContent(sn.path, sn.client)
181         if status == syscall.ENOENT {
182                 sn.Inode.ForgetPersistent()
183         }
184         return &statFH{data: []byte(content)}, fuse.FOPEN_DIRECT_IO, status
185 }
186
187 /* The statFH structure aims at dislaying the counters dynamically.
188  * It allows the Kernel to read data as I/O without having to specify files sizes, as they may evolve dynamically.
189  */
190 type statFH struct {
191         data []byte
192 }
193
194 var _ = (fs.FileReader)((*statFH)(nil))
195
196 func (fh *statFH) Read(ctx context.Context, data []byte, off int64) (fuse.ReadResult, syscall.Errno) {
197         end := int(off) + len(data)
198         if end > len(fh.data) {
199                 end = len(fh.data)
200         }
201         return fuse.ReadResultData(fh.data[off:end]), fs.OK
202 }
203
204 //NewStatsFileSystem creates the fs for the stat segment.
205 func NewStatsFileSystem(sc *statsclient.StatsClient) (root fs.InodeEmbedder, err error) {
206         return &rootNode{client: sc}, nil
207 }