Add statsclient - pure Go implementation for stats API
[govpp.git] / adapter / statsclient / statsclient.go
1 // Copyright (c) 2019 Cisco and/or its affiliates.
2 //
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 package statsclient
16
17 import (
18         "bytes"
19         "fmt"
20         "net"
21         "os"
22         "regexp"
23         "sync/atomic"
24         "syscall"
25         "time"
26         "unsafe"
27
28         "github.com/ftrvxmtrx/fd"
29         logger "github.com/sirupsen/logrus"
30
31         "git.fd.io/govpp.git/adapter"
32 )
33
34 var (
35         // Debug is global variable that determines debug mode
36         Debug = os.Getenv("DEBUG_GOVPP_STATS") != ""
37
38         // Log is global logger
39         Log = logger.New()
40 )
41
42 // init initializes global logger, which logs debug level messages to stdout.
43 func init() {
44         Log.Out = os.Stdout
45         if Debug {
46                 Log.Level = logger.DebugLevel
47                 Log.Debug("enabled debug mode")
48         }
49 }
50
51 // StatsClient is the pure Go implementation for VPP stats API.
52 type StatsClient struct {
53         sockAddr string
54
55         currentEpoch    int64
56         sharedHeader    []byte
57         directoryVector uintptr
58         memorySize      int
59 }
60
61 // NewStatsClient returns new VPP stats API client.
62 func NewStatsClient(socketName string) *StatsClient {
63         return &StatsClient{
64                 sockAddr: socketName,
65         }
66 }
67
68 func (c *StatsClient) Connect() error {
69         var sockName string
70         if c.sockAddr == "" {
71                 sockName = adapter.DefaultStatsSocket
72         } else {
73                 sockName = c.sockAddr
74         }
75
76         if _, err := os.Stat(sockName); err != nil {
77                 if os.IsNotExist(err) {
78                         return fmt.Errorf("stats socket file %q does not exists, ensure that VPP is running with `statseg { ... }` section in config", sockName)
79                 }
80                 return fmt.Errorf("stats socket file error: %v", err)
81         }
82
83         if err := c.statSegmentConnect(sockName); err != nil {
84                 return err
85         }
86
87         return nil
88 }
89
90 const statshmFilename = "statshm"
91
92 func (c *StatsClient) statSegmentConnect(sockName string) error {
93         addr := &net.UnixAddr{
94                 Net:  "unixpacket",
95                 Name: sockName,
96         }
97
98         Log.Debugf("connecting to: %v", addr)
99
100         conn, err := net.DialUnix(addr.Net, nil, addr)
101         if err != nil {
102                 Log.Warnf("connecting to socket %s failed: %s", addr, err)
103                 return err
104         }
105         defer func() {
106                 if err := conn.Close(); err != nil {
107                         Log.Warnf("closing socket failed: %v", err)
108                 }
109         }()
110
111         Log.Debugf("connected to socket: %v", addr)
112
113         files, err := fd.Get(conn, 1, []string{statshmFilename})
114         if err != nil {
115                 return fmt.Errorf("getting file descriptor over socket failed: %v", err)
116         } else if len(files) == 0 {
117                 return fmt.Errorf("no files received over socket")
118         }
119         defer func() {
120                 for _, f := range files {
121                         if err := f.Close(); err != nil {
122                                 Log.Warnf("closing file %s failed: %v", f.Name(), err)
123                         }
124                 }
125         }()
126
127         Log.Debugf("received %d files over socket", len(files))
128
129         f := files[0]
130
131         info, err := f.Stat()
132         if err != nil {
133                 return err
134         }
135
136         size := int(info.Size())
137
138         Log.Debugf("fd: name=%v size=%v", info.Name(), size)
139
140         data, err := syscall.Mmap(int(f.Fd()), 0, size, syscall.PROT_READ, syscall.MAP_SHARED)
141         if err != nil {
142                 Log.Warnf("mapping shared memory failed: %v", err)
143                 return fmt.Errorf("mapping shared memory failed: %v", err)
144         }
145
146         Log.Debugf("successfuly mapped shared memory")
147
148         c.sharedHeader = data
149         c.memorySize = size
150
151         return nil
152 }
153
154 func (c *StatsClient) Disconnect() error {
155         err := syscall.Munmap(c.sharedHeader)
156         if err != nil {
157                 Log.Warnf("unmapping shared memory failed: %v", err)
158                 return fmt.Errorf("unmapping shared memory failed: %v", err)
159         }
160
161         Log.Debugf("successfuly unmapped shared memory")
162
163         return nil
164 }
165
166 func nameMatches(name string, patterns []string) bool {
167         if len(patterns) == 0 {
168                 return true
169         }
170         for _, pattern := range patterns {
171                 matched, err := regexp.MatchString(pattern, name)
172                 if err == nil && matched {
173                         return true
174                 }
175         }
176         return false
177 }
178
179 func (c *StatsClient) ListStats(patterns ...string) (statNames []string, err error) {
180         sa := c.accessStart()
181         if sa == nil {
182                 return nil, fmt.Errorf("access failed")
183         }
184
185         dirOffset, _, _ := c.readOffsets()
186         Log.Debugf("dirOffset: %v", dirOffset)
187
188         vecLen := vectorLen(unsafe.Pointer(&c.sharedHeader[dirOffset]))
189         Log.Debugf("vecLen: %v", vecLen)
190         Log.Debugf("unsafe.Sizeof(statSegDirectoryEntry{}): %v", unsafe.Sizeof(statSegDirectoryEntry{}))
191
192         for i := uint64(0); i < vecLen; i++ {
193                 offset := uintptr(i) * unsafe.Sizeof(statSegDirectoryEntry{})
194                 dirEntry := (*statSegDirectoryEntry)(add(unsafe.Pointer(&c.sharedHeader[dirOffset]), offset))
195
196                 nul := bytes.IndexByte(dirEntry.name[:], '\x00')
197                 if nul < 0 {
198                         Log.Warnf("no zero byte found for: %q", dirEntry.name[:])
199                         continue
200                 }
201                 name := string(dirEntry.name[:nul])
202
203                 Log.Debugf(" %80q (type: %v, data: %d, offset: %d) ", name, dirEntry.directoryType, dirEntry.unionData, dirEntry.offsetVector)
204
205                 if nameMatches(name, patterns) {
206                         statNames = append(statNames, name)
207                 }
208
209                 // TODO: copy the listed entries elsewhere
210         }
211
212         if !c.accessEnd(sa) {
213                 return nil, adapter.ErrStatDirBusy
214         }
215
216         c.currentEpoch = sa.epoch
217
218         return statNames, nil
219 }
220
221 func (c *StatsClient) DumpStats(patterns ...string) (entries []*adapter.StatEntry, err error) {
222         epoch, _ := c.readEpoch()
223         if c.currentEpoch > 0 && c.currentEpoch != epoch { // TODO: do list stats before dump
224                 return nil, fmt.Errorf("old data")
225         }
226
227         sa := c.accessStart()
228         if sa == nil {
229                 return nil, fmt.Errorf("access failed")
230         }
231
232         dirOffset, _, _ := c.readOffsets()
233         vecLen := vectorLen(unsafe.Pointer(&c.sharedHeader[dirOffset]))
234
235         for i := uint64(0); i < vecLen; i++ {
236                 offset := uintptr(i) * unsafe.Sizeof(statSegDirectoryEntry{})
237                 dirEntry := (*statSegDirectoryEntry)(add(unsafe.Pointer(&c.sharedHeader[dirOffset]), offset))
238
239                 entry := c.copyData(dirEntry)
240                 if nameMatches(entry.Name, patterns) {
241                         entries = append(entries, &entry)
242                 }
243         }
244
245         if !c.accessEnd(sa) {
246                 return nil, adapter.ErrStatDumpBusy
247         }
248
249         return entries, nil
250 }
251
252 func (c *StatsClient) copyData(dirEntry *statSegDirectoryEntry) (statEntry adapter.StatEntry) {
253         name := dirEntry.name[:]
254         if nul := bytes.IndexByte(name, '\x00'); nul < 0 {
255                 Log.Warnf("no zero byte found for: %q", dirEntry.name[:])
256         } else {
257                 name = dirEntry.name[:nul]
258         }
259
260         statEntry.Name = string(name)
261         statEntry.Type = adapter.StatType(dirEntry.directoryType)
262
263         Log.Debugf(" - %s (type: %v, data: %v, offset: %v) ", statEntry.Name, statEntry.Type, dirEntry.unionData, dirEntry.offsetVector)
264
265         switch statEntry.Type {
266         case adapter.ScalarIndex:
267                 statEntry.Data = adapter.ScalarStat(dirEntry.unionData)
268
269         case adapter.ErrorIndex:
270                 _, errOffset, _ := c.readOffsets()
271                 offsetVector := unsafe.Pointer(&c.sharedHeader[errOffset])
272                 vecLen := vectorLen(offsetVector)
273
274                 var errData adapter.Counter
275                 for i := uint64(0); i < vecLen; i++ {
276                         cb := *(*uint64)(add(offsetVector, uintptr(i)*unsafe.Sizeof(uint64(0))))
277                         offset := uintptr(cb) + uintptr(dirEntry.unionData)*unsafe.Sizeof(adapter.Counter(0))
278                         val := *(*adapter.Counter)(add(unsafe.Pointer(&c.sharedHeader[0]), offset))
279                         errData += val
280                 }
281                 statEntry.Data = adapter.ErrorStat(errData)
282
283         case adapter.SimpleCounterVector:
284                 if dirEntry.unionData == 0 {
285                         Log.Debugf("\toffset is not valid")
286                         break
287                 } else if dirEntry.unionData >= uint64(len(c.sharedHeader)) {
288                         Log.Debugf("\toffset out of range")
289                         break
290                 }
291
292                 simpleCounter := unsafe.Pointer(&c.sharedHeader[dirEntry.unionData]) // offset
293                 vecLen := vectorLen(simpleCounter)
294                 offsetVector := add(unsafe.Pointer(&c.sharedHeader[0]), uintptr(dirEntry.offsetVector))
295
296                 data := make([][]adapter.Counter, vecLen)
297                 for i := uint64(0); i < vecLen; i++ {
298                         cb := *(*uint64)(add(offsetVector, uintptr(i)*unsafe.Sizeof(uint64(0))))
299                         counterVec := unsafe.Pointer(&c.sharedHeader[uintptr(cb)])
300                         vecLen2 := vectorLen(counterVec)
301                         for j := uint64(0); j < vecLen2; j++ {
302                                 offset := uintptr(j) * unsafe.Sizeof(adapter.Counter(0))
303                                 val := *(*adapter.Counter)(add(counterVec, offset))
304                                 data[i] = append(data[i], val)
305                         }
306                 }
307                 statEntry.Data = adapter.SimpleCounterStat(data)
308
309         case adapter.CombinedCounterVector:
310                 if dirEntry.unionData == 0 {
311                         Log.Debugf("\toffset is not valid")
312                         break
313                 } else if dirEntry.unionData >= uint64(len(c.sharedHeader)) {
314                         Log.Debugf("\toffset out of range")
315                         break
316                 }
317
318                 combinedCounter := unsafe.Pointer(&c.sharedHeader[dirEntry.unionData]) // offset
319                 vecLen := vectorLen(combinedCounter)
320                 offsetVector := add(unsafe.Pointer(&c.sharedHeader[0]), uintptr(dirEntry.offsetVector))
321
322                 data := make([][]adapter.CombinedCounter, vecLen)
323                 for i := uint64(0); i < vecLen; i++ {
324                         cb := *(*uint64)(add(offsetVector, uintptr(i)*unsafe.Sizeof(uint64(0))))
325                         counterVec := unsafe.Pointer(&c.sharedHeader[uintptr(cb)])
326                         vecLen2 := vectorLen(counterVec)
327                         for j := uint64(0); j < vecLen2; j++ {
328                                 offset := uintptr(j) * unsafe.Sizeof(adapter.CombinedCounter{})
329                                 val := *(*adapter.CombinedCounter)(add(counterVec, offset))
330                                 data[i] = append(data[i], val)
331                         }
332                 }
333                 statEntry.Data = adapter.CombinedCounterStat(data)
334
335         case adapter.NameVector:
336                 if dirEntry.unionData == 0 {
337                         Log.Debugf("\toffset is not valid")
338                         break
339                 } else if dirEntry.unionData >= uint64(len(c.sharedHeader)) {
340                         Log.Debugf("\toffset out of range")
341                         break
342                 }
343
344                 nameVector := unsafe.Pointer(&c.sharedHeader[dirEntry.unionData]) // offset
345                 vecLen := vectorLen(nameVector)
346                 offsetVector := add(unsafe.Pointer(&c.sharedHeader[0]), uintptr(dirEntry.offsetVector))
347
348                 data := make([]adapter.Name, vecLen)
349                 for i := uint64(0); i < vecLen; i++ {
350                         cb := *(*uint64)(add(offsetVector, uintptr(i)*unsafe.Sizeof(uint64(0))))
351                         nameVec := unsafe.Pointer(&c.sharedHeader[uintptr(cb)])
352                         vecLen2 := vectorLen(nameVec)
353
354                         var nameStr []byte
355                         for j := uint64(0); j < vecLen2; j++ {
356                                 offset := uintptr(j) * unsafe.Sizeof(byte(0))
357                                 val := *(*byte)(add(nameVec, offset))
358                                 if val > 0 {
359                                         nameStr = append(nameStr, val)
360                                 }
361                         }
362                         data[i] = adapter.Name(nameStr)
363                 }
364                 statEntry.Data = adapter.NameStat(data)
365
366         default:
367                 Log.Warnf("Unknown type %d for stat entry: %s", statEntry.Type, statEntry.Name)
368         }
369
370         Log.Debugf("\tentry data: %#v", statEntry.Data)
371
372         return statEntry
373 }
374
375 type statDirectoryType int32
376
377 func (t statDirectoryType) String() string {
378         return adapter.StatType(t).String()
379 }
380
381 type statSegDirectoryEntry struct {
382         directoryType statDirectoryType
383         // unionData can represent: offset, index or value
384         unionData    uint64
385         offsetVector uint64
386         name         [128]byte
387 }
388
389 type statSegSharedHeader struct {
390         version         uint64
391         epoch           int64
392         inProgress      int64
393         directoryOffset int64
394         errorOffset     int64
395         statsOffset     int64
396 }
397
398 func (c *StatsClient) readVersion() uint64 {
399         header := *(*statSegSharedHeader)(unsafe.Pointer(&c.sharedHeader[0]))
400         version := atomic.LoadUint64(&header.version)
401         return version
402 }
403
404 func (c *StatsClient) readEpoch() (int64, bool) {
405         header := *(*statSegSharedHeader)(unsafe.Pointer(&c.sharedHeader[0]))
406         epoch := atomic.LoadInt64(&header.epoch)
407         inprog := atomic.LoadInt64(&header.inProgress)
408         return epoch, inprog != 0
409 }
410
411 func (c *StatsClient) readOffsets() (dir, err, stat int64) {
412         header := *(*statSegSharedHeader)(unsafe.Pointer(&c.sharedHeader[0]))
413         dirOffset := atomic.LoadInt64(&header.directoryOffset)
414         errOffset := atomic.LoadInt64(&header.errorOffset)
415         statOffset := atomic.LoadInt64(&header.statsOffset)
416         return dirOffset, errOffset, statOffset
417 }
418
419 type statSegAccess struct {
420         epoch int64
421 }
422
423 var maxWaitInProgress = 1 * time.Second
424
425 func (c *StatsClient) accessStart() *statSegAccess {
426         epoch, inprog := c.readEpoch()
427         t := time.Now()
428         for inprog {
429                 if time.Since(t) > maxWaitInProgress {
430                         return nil
431                 }
432                 epoch, inprog = c.readEpoch()
433         }
434         return &statSegAccess{
435                 epoch: epoch,
436         }
437 }
438
439 func (c *StatsClient) accessEnd(acc *statSegAccess) bool {
440         epoch, inprog := c.readEpoch()
441         if acc.epoch != epoch || inprog {
442                 return false
443         }
444         return true
445 }
446
447 type vecHeader struct {
448         length     uint64
449         vectorData [0]uint8
450 }
451
452 func vectorLen(v unsafe.Pointer) uint64 {
453         vec := *(*vecHeader)(unsafe.Pointer(uintptr(v) - unsafe.Sizeof(uintptr(0))))
454         return vec.length
455 }
456
457 //go:nosplit
458 func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
459         return unsafe.Pointer(uintptr(p) + x)
460 }