Compare commits

...

4 Commits

Author SHA1 Message Date
gadotroee
8c187179b0 Adding tests to the bucket statistics (#1160) 2022-06-23 11:04:12 +03:00
lirazyehezkel
d7d802830f TRA-4274 fix ui common linter issues (#1161)
* fix lint issues

* fix all ui-common line issues

* max warnings 0
2022-06-22 09:56:27 +03:00
AmitUp9
4d64dd4b04 TRA-4602_timeline bar charts to traffic statistics (#1159)
* pie chart for protocols and methods by requests and volume

* protocols legend

* timeline bar chart component created

* timeline can view requests and volume

* sorting the bra charts by timestemp

* disable view of <1% pieces in pie

* space added to the end of the file

* package.json update

* cr fixes

* remove spave

* remove unnecessary react fragment

Co-authored-by: Liraz Yehezkel <lirazy@up9.com>
2022-06-21 14:54:56 +03:00
gadotroee
9a40895e9c Add timing endpoint (timeline data) for stats (#1157) 2022-06-21 14:33:04 +03:00
18 changed files with 685 additions and 130 deletions

View File

@@ -83,6 +83,11 @@ func GetAccumulativeStats(c *gin.Context) {
c.JSON(http.StatusOK, providers.GetAccumulativeStats()) c.JSON(http.StatusOK, providers.GetAccumulativeStats())
} }
func GetAccumulativeStatsTiming(c *gin.Context) {
// for now hardcoded 10 bars of 5 minutes interval
c.JSON(http.StatusOK, providers.GetAccumulativeStatsTiming(300, 10))
}
func GetCurrentResolvingInformation(c *gin.Context) { func GetCurrentResolvingInformation(c *gin.Context) {
c.JSON(http.StatusOK, holder.GetResolver().GetMap()) c.JSON(http.StatusOK, holder.GetResolver().GetMap())
} }

View File

@@ -2,6 +2,7 @@ package providers
import ( import (
"reflect" "reflect"
"sync"
"time" "time"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
@@ -19,18 +20,18 @@ type GeneralStats struct {
type BucketStats []*TimeFrameStatsValue type BucketStats []*TimeFrameStatsValue
type TimeFrameStatsValue struct { type TimeFrameStatsValue struct {
BucketTime time.Time BucketTime time.Time `json:"timestamp"`
ProtocolStats map[string]ProtocolStats ProtocolStats map[string]ProtocolStats `json:"protocols"`
} }
type ProtocolStats struct { type ProtocolStats struct {
MethodsStats map[string]*SizeAndEntriesCount MethodsStats map[string]*SizeAndEntriesCount `json:"methods"`
Color string Color string `json:"color"`
} }
type SizeAndEntriesCount struct { type SizeAndEntriesCount struct {
EntriesCount int EntriesCount int `json:"entriesCount"`
VolumeInBytes int VolumeInBytes int `json:"volumeInBytes"`
} }
type AccumulativeStatsCounter struct { type AccumulativeStatsCounter struct {
@@ -45,9 +46,19 @@ type AccumulativeStatsProtocol struct {
Methods []*AccumulativeStatsCounter `json:"methods"` Methods []*AccumulativeStatsCounter `json:"methods"`
} }
type AccumulativeStatsProtocolTime struct {
ProtocolsData []*AccumulativeStatsProtocol `json:"protocols"`
Time int64 `json:"timestamp"`
}
var ( var (
generalStats = GeneralStats{} generalStats = GeneralStats{}
bucketsStats = BucketStats{} bucketsStats = BucketStats{}
bucketStatsLocker = sync.Mutex{}
)
const (
InternalBucketThreshold = time.Minute * 1
) )
func ResetGeneralStats() { func ResetGeneralStats() {
@@ -59,58 +70,27 @@ func GetGeneralStats() GeneralStats {
} }
func GetAccumulativeStats() []*AccumulativeStatsProtocol { func GetAccumulativeStats() []*AccumulativeStatsProtocol {
bucketStatsCopy := BucketStats{} bucketStatsCopy := getBucketStatsCopy()
if err := copier.Copy(&bucketStatsCopy, bucketsStats); err != nil { if len(bucketStatsCopy) == 0 {
logger.Log.Errorf("Error while copying src stats into temporary copied object")
return make([]*AccumulativeStatsProtocol, 0) return make([]*AccumulativeStatsProtocol, 0)
} }
result := make(map[string]*AccumulativeStatsProtocol, 0) methodsPerProtocolAggregated, protocolToColor := getAggregatedStatsAllTime(bucketStatsCopy)
methodsPerProtocolAggregated := make(map[string]map[string]*AccumulativeStatsCounter, 0)
for _, countersOfTimeFrame := range bucketStatsCopy {
for protocolName, value := range countersOfTimeFrame.ProtocolStats {
if _, found := result[protocolName]; !found { return convertAccumulativeStatsDictToArray(methodsPerProtocolAggregated, protocolToColor)
result[protocolName] = &AccumulativeStatsProtocol{ }
AccumulativeStatsCounter: AccumulativeStatsCounter{
Name: protocolName,
EntriesCount: 0,
VolumeSizeBytes: 0,
},
Color: value.Color,
}
}
if _, found := methodsPerProtocolAggregated[protocolName]; !found {
methodsPerProtocolAggregated[protocolName] = map[string]*AccumulativeStatsCounter{}
}
for method, countersValue := range value.MethodsStats { func GetAccumulativeStatsTiming(intervalSeconds int, numberOfBars int) []*AccumulativeStatsProtocolTime {
if _, found := methodsPerProtocolAggregated[protocolName][method]; !found { bucketStatsCopy := getBucketStatsCopy()
methodsPerProtocolAggregated[protocolName][method] = &AccumulativeStatsCounter{ if len(bucketStatsCopy) == 0 {
Name: method, return make([]*AccumulativeStatsProtocolTime, 0)
EntriesCount: 0,
VolumeSizeBytes: 0,
}
}
result[protocolName].AccumulativeStatsCounter.EntriesCount += countersValue.EntriesCount
methodsPerProtocolAggregated[protocolName][method].EntriesCount += countersValue.EntriesCount
result[protocolName].AccumulativeStatsCounter.VolumeSizeBytes += countersValue.VolumeInBytes
methodsPerProtocolAggregated[protocolName][method].VolumeSizeBytes += countersValue.VolumeInBytes
}
}
} }
finalResult := make([]*AccumulativeStatsProtocol, 0) firstBucketTime := getFirstBucketTime(time.Now().UTC(), intervalSeconds, numberOfBars)
for _, value := range result {
methodsForProtocol := make([]*AccumulativeStatsCounter, 0) methodsPerProtocolPerTimeAggregated, protocolToColor := getAggregatedResultTimingFromSpecificTime(intervalSeconds, bucketStatsCopy, firstBucketTime)
for _, methodValue := range methodsPerProtocolAggregated[value.Name] {
methodsForProtocol = append(methodsForProtocol, methodValue) return convertAccumulativeStatsTimelineDictToArray(methodsPerProtocolPerTimeAggregated, protocolToColor)
}
value.Methods = methodsForProtocol
finalResult = append(finalResult, value)
}
return finalResult
} }
func EntryAdded(size int, summery *api.BaseEntry) { func EntryAdded(size int, summery *api.BaseEntry) {
@@ -129,7 +109,8 @@ func EntryAdded(size int, summery *api.BaseEntry) {
} }
func addToBucketStats(size int, summery *api.BaseEntry) { func addToBucketStats(size int, summery *api.BaseEntry) {
entryTimeBucketRounded := time.Unix(summery.Timestamp, 0).Round(time.Minute * 1) entryTimeBucketRounded := getBucketFromTimeStamp(summery.Timestamp)
if len(bucketsStats) == 0 { if len(bucketsStats) == 0 {
bucketsStats = append(bucketsStats, &TimeFrameStatsValue{ bucketsStats = append(bucketsStats, &TimeFrameStatsValue{
BucketTime: entryTimeBucketRounded, BucketTime: entryTimeBucketRounded,
@@ -160,3 +141,148 @@ func addToBucketStats(size int, summery *api.BaseEntry) {
bucketOfEntry.ProtocolStats[summery.Protocol.Abbreviation].MethodsStats[summery.Method].EntriesCount += 1 bucketOfEntry.ProtocolStats[summery.Protocol.Abbreviation].MethodsStats[summery.Method].EntriesCount += 1
bucketOfEntry.ProtocolStats[summery.Protocol.Abbreviation].MethodsStats[summery.Method].VolumeInBytes += size bucketOfEntry.ProtocolStats[summery.Protocol.Abbreviation].MethodsStats[summery.Method].VolumeInBytes += size
} }
func getBucketFromTimeStamp(timestamp int64) time.Time {
entryTimeStampAsTime := time.UnixMilli(timestamp)
return entryTimeStampAsTime.Add(-1 * InternalBucketThreshold / 2).Round(InternalBucketThreshold)
}
func getFirstBucketTime(endTime time.Time, intervalSeconds int, numberOfBars int) time.Time {
lastBucketTime := endTime.Add(-1 * time.Second * time.Duration(intervalSeconds) / 2).Round(time.Second * time.Duration(intervalSeconds))
firstBucketTime := lastBucketTime.Add(-1 * time.Second * time.Duration(intervalSeconds*(numberOfBars-1)))
return firstBucketTime
}
func convertAccumulativeStatsTimelineDictToArray(methodsPerProtocolPerTimeAggregated map[time.Time]map[string]map[string]*AccumulativeStatsCounter, protocolToColor map[string]string) []*AccumulativeStatsProtocolTime {
finalResult := make([]*AccumulativeStatsProtocolTime, 0)
for timeKey, item := range methodsPerProtocolPerTimeAggregated {
protocolsData := make([]*AccumulativeStatsProtocol, 0)
for protocolName := range item {
entriesCount := 0
volumeSizeBytes := 0
methods := make([]*AccumulativeStatsCounter, 0)
for _, methodAccData := range methodsPerProtocolPerTimeAggregated[timeKey][protocolName] {
entriesCount += methodAccData.EntriesCount
volumeSizeBytes += methodAccData.VolumeSizeBytes
methods = append(methods, methodAccData)
}
protocolsData = append(protocolsData, &AccumulativeStatsProtocol{
AccumulativeStatsCounter: AccumulativeStatsCounter{
Name: protocolName,
EntriesCount: entriesCount,
VolumeSizeBytes: volumeSizeBytes,
},
Color: protocolToColor[protocolName],
Methods: methods,
})
}
finalResult = append(finalResult, &AccumulativeStatsProtocolTime{
Time: timeKey.UnixMilli(),
ProtocolsData: protocolsData,
})
}
return finalResult
}
func convertAccumulativeStatsDictToArray(methodsPerProtocolAggregated map[string]map[string]*AccumulativeStatsCounter, protocolToColor map[string]string) []*AccumulativeStatsProtocol {
protocolsData := make([]*AccumulativeStatsProtocol, 0)
for protocolName, value := range methodsPerProtocolAggregated {
entriesCount := 0
volumeSizeBytes := 0
methods := make([]*AccumulativeStatsCounter, 0)
for _, methodAccData := range value {
entriesCount += methodAccData.EntriesCount
volumeSizeBytes += methodAccData.VolumeSizeBytes
methods = append(methods, methodAccData)
}
protocolsData = append(protocolsData, &AccumulativeStatsProtocol{
AccumulativeStatsCounter: AccumulativeStatsCounter{
Name: protocolName,
EntriesCount: entriesCount,
VolumeSizeBytes: volumeSizeBytes,
},
Color: protocolToColor[protocolName],
Methods: methods,
})
}
return protocolsData
}
func getBucketStatsCopy() BucketStats {
bucketStatsCopy := BucketStats{}
bucketStatsLocker.Lock()
if err := copier.Copy(&bucketStatsCopy, bucketsStats); err != nil {
logger.Log.Errorf("Error while copying src stats into temporary copied object")
return nil
}
bucketStatsLocker.Unlock()
return bucketStatsCopy
}
func getAggregatedResultTimingFromSpecificTime(intervalSeconds int, bucketStats BucketStats, firstBucketTime time.Time) (map[time.Time]map[string]map[string]*AccumulativeStatsCounter, map[string]string) {
protocolToColor := map[string]string{}
methodsPerProtocolPerTimeAggregated := map[time.Time]map[string]map[string]*AccumulativeStatsCounter{}
bucketStatsIndex := len(bucketStats) - 1
for bucketStatsIndex >= 0 {
currentBucketTime := bucketStats[bucketStatsIndex].BucketTime
if currentBucketTime.After(firstBucketTime) || currentBucketTime.Equal(firstBucketTime) {
resultBucketRoundedKey := currentBucketTime.Add(-1 * time.Second * time.Duration(intervalSeconds) / 2).Round(time.Second * time.Duration(intervalSeconds))
for protocolName, data := range bucketStats[bucketStatsIndex].ProtocolStats {
if _, ok := protocolToColor[protocolName]; !ok {
protocolToColor[protocolName] = data.Color
}
for methodName, dataOfMethod := range data.MethodsStats {
if _, ok := methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey]; !ok {
methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey] = map[string]map[string]*AccumulativeStatsCounter{}
}
if _, ok := methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey][protocolName]; !ok {
methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey][protocolName] = map[string]*AccumulativeStatsCounter{}
}
if _, ok := methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey][protocolName][methodName]; !ok {
methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey][protocolName][methodName] = &AccumulativeStatsCounter{
Name: methodName,
EntriesCount: 0,
VolumeSizeBytes: 0,
}
}
methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey][protocolName][methodName].EntriesCount += dataOfMethod.EntriesCount
methodsPerProtocolPerTimeAggregated[resultBucketRoundedKey][protocolName][methodName].VolumeSizeBytes += dataOfMethod.VolumeInBytes
}
}
}
bucketStatsIndex--
}
return methodsPerProtocolPerTimeAggregated, protocolToColor
}
func getAggregatedStatsAllTime(bucketStatsCopy BucketStats) (map[string]map[string]*AccumulativeStatsCounter, map[string]string) {
protocolToColor := make(map[string]string, 0)
methodsPerProtocolAggregated := make(map[string]map[string]*AccumulativeStatsCounter, 0)
for _, countersOfTimeFrame := range bucketStatsCopy {
for protocolName, value := range countersOfTimeFrame.ProtocolStats {
if _, ok := protocolToColor[protocolName]; !ok {
protocolToColor[protocolName] = value.Color
}
for method, countersValue := range value.MethodsStats {
if _, found := methodsPerProtocolAggregated[protocolName]; !found {
methodsPerProtocolAggregated[protocolName] = map[string]*AccumulativeStatsCounter{}
}
if _, found := methodsPerProtocolAggregated[protocolName][method]; !found {
methodsPerProtocolAggregated[protocolName][method] = &AccumulativeStatsCounter{
Name: method,
EntriesCount: 0,
VolumeSizeBytes: 0,
}
}
methodsPerProtocolAggregated[protocolName][method].EntriesCount += countersValue.EntriesCount
methodsPerProtocolAggregated[protocolName][method].VolumeSizeBytes += countersValue.VolumeInBytes
}
}
}
return methodsPerProtocolAggregated, protocolToColor
}

View File

@@ -0,0 +1,331 @@
package providers
import (
"fmt"
"reflect"
"testing"
"time"
)
func TestGetBucketOfTimeStamp(t *testing.T) {
tests := map[int64]time.Time{
time.Date(2022, time.Month(1), 1, 10, 34, 45, 0, time.Local).UnixMilli(): time.Date(2022, time.Month(1), 1, 10, 34, 00, 0, time.Local),
time.Date(2022, time.Month(1), 1, 10, 34, 00, 0, time.Local).UnixMilli(): time.Date(2022, time.Month(1), 1, 10, 34, 00, 0, time.Local),
time.Date(2022, time.Month(1), 1, 10, 59, 01, 0, time.Local).UnixMilli(): time.Date(2022, time.Month(1), 1, 10, 59, 00, 0, time.Local),
}
for key, value := range tests {
t.Run(fmt.Sprintf("%v", key), func(t *testing.T) {
actual := getBucketFromTimeStamp(key)
if actual != value {
t.Errorf("unexpected result - expected: %v, actual: %v", value, actual)
}
})
}
}
type DataForBucketBorderFunction struct {
EndTime time.Time
IntervalInSeconds int
NumberOfBars int
}
func TestGetBucketBorders(t *testing.T) {
tests := map[DataForBucketBorderFunction]time.Time{
DataForBucketBorderFunction{
time.Date(2022, time.Month(1), 1, 10, 34, 45, 0, time.UTC),
300,
10,
}: time.Date(2022, time.Month(1), 1, 9, 45, 0, 0, time.UTC),
DataForBucketBorderFunction{
time.Date(2022, time.Month(1), 1, 10, 35, 45, 0, time.UTC),
60,
5,
}: time.Date(2022, time.Month(1), 1, 10, 31, 00, 0, time.UTC),
}
for key, value := range tests {
t.Run(fmt.Sprintf("%v", key), func(t *testing.T) {
actual := getFirstBucketTime(key.EndTime, key.IntervalInSeconds, key.NumberOfBars)
if actual != value {
t.Errorf("unexpected result - expected: %v, actual: %v", value, actual)
}
})
}
}
func TestGetAggregatedStatsAllTime(t *testing.T) {
bucketStatsForTest := BucketStats{
&TimeFrameStatsValue{
BucketTime: time.Date(2022, time.Month(1), 1, 10, 00, 00, 0, time.UTC),
ProtocolStats: map[string]ProtocolStats{
"http": {
MethodsStats: map[string]*SizeAndEntriesCount{
"get": {
EntriesCount: 1,
VolumeInBytes: 2,
},
"post": {
EntriesCount: 2,
VolumeInBytes: 3,
},
},
},
"kafka": {
MethodsStats: map[string]*SizeAndEntriesCount{
"listTopics": {
EntriesCount: 5,
VolumeInBytes: 6,
},
},
},
},
},
&TimeFrameStatsValue{
BucketTime: time.Date(2022, time.Month(1), 1, 10, 01, 00, 0, time.UTC),
ProtocolStats: map[string]ProtocolStats{
"http": {
MethodsStats: map[string]*SizeAndEntriesCount{
"get": {
EntriesCount: 1,
VolumeInBytes: 2,
},
"post": {
EntriesCount: 2,
VolumeInBytes: 3,
},
},
},
"redis": {
MethodsStats: map[string]*SizeAndEntriesCount{
"set": {
EntriesCount: 5,
VolumeInBytes: 6,
},
},
},
},
},
}
expected := map[string]map[string]*AccumulativeStatsCounter{
"http": {
"post": {
Name: "post",
EntriesCount: 4,
VolumeSizeBytes: 6,
},
"get": {
Name: "get",
EntriesCount: 2,
VolumeSizeBytes: 4,
},
},
"kafka": {
"listTopics": {
Name: "listTopics",
EntriesCount: 5,
VolumeSizeBytes: 6,
},
},
"redis": {
"set": {
Name: "set",
EntriesCount: 5,
VolumeSizeBytes: 6,
},
},
}
actual, _ := getAggregatedStatsAllTime(bucketStatsForTest)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result - expected: %v, actual: %v", 3, len(actual))
}
}
func TestGetAggregatedStatsFromSpecificTime(t *testing.T) {
bucketStatsForTest := BucketStats{
&TimeFrameStatsValue{
BucketTime: time.Date(2022, time.Month(1), 1, 10, 00, 00, 0, time.UTC),
ProtocolStats: map[string]ProtocolStats{
"http": {
MethodsStats: map[string]*SizeAndEntriesCount{
"get": {
EntriesCount: 1,
VolumeInBytes: 2,
},
},
},
"kafka": {
MethodsStats: map[string]*SizeAndEntriesCount{
"listTopics": {
EntriesCount: 5,
VolumeInBytes: 6,
},
},
},
},
},
&TimeFrameStatsValue{
BucketTime: time.Date(2022, time.Month(1), 1, 10, 01, 00, 0, time.UTC),
ProtocolStats: map[string]ProtocolStats{
"http": {
MethodsStats: map[string]*SizeAndEntriesCount{
"get": {
EntriesCount: 1,
VolumeInBytes: 2,
},
"post": {
EntriesCount: 2,
VolumeInBytes: 3,
},
},
},
"redis": {
MethodsStats: map[string]*SizeAndEntriesCount{
"set": {
EntriesCount: 5,
VolumeInBytes: 6,
},
},
},
},
},
}
expected := map[time.Time]map[string]map[string]*AccumulativeStatsCounter{
time.Date(2022, time.Month(1), 1, 10, 00, 00, 0, time.UTC): {
"http": {
"post": {
Name: "post",
EntriesCount: 2,
VolumeSizeBytes: 3,
},
"get": {
Name: "get",
EntriesCount: 2,
VolumeSizeBytes: 4,
},
},
"kafka": {
"listTopics": {
Name: "listTopics",
EntriesCount: 5,
VolumeSizeBytes: 6,
},
},
"redis": {
"set": {
Name: "set",
EntriesCount: 5,
VolumeSizeBytes: 6,
},
},
},
}
actual, _ := getAggregatedResultTimingFromSpecificTime(300, bucketStatsForTest, time.Date(2022, time.Month(1), 1, 10, 00, 00, 0, time.UTC))
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result - expected: %v, actual: %v", 3, len(actual))
}
}
func TestGetAggregatedStatsFromSpecificTimeMultipleBuckets(t *testing.T) {
bucketStatsForTest := BucketStats{
&TimeFrameStatsValue{
BucketTime: time.Date(2022, time.Month(1), 1, 10, 00, 00, 0, time.UTC),
ProtocolStats: map[string]ProtocolStats{
"http": {
MethodsStats: map[string]*SizeAndEntriesCount{
"get": {
EntriesCount: 1,
VolumeInBytes: 2,
},
},
},
"kafka": {
MethodsStats: map[string]*SizeAndEntriesCount{
"listTopics": {
EntriesCount: 5,
VolumeInBytes: 6,
},
},
},
},
},
&TimeFrameStatsValue{
BucketTime: time.Date(2022, time.Month(1), 1, 10, 01, 00, 0, time.UTC),
ProtocolStats: map[string]ProtocolStats{
"http": {
MethodsStats: map[string]*SizeAndEntriesCount{
"get": {
EntriesCount: 1,
VolumeInBytes: 2,
},
"post": {
EntriesCount: 2,
VolumeInBytes: 3,
},
},
},
"redis": {
MethodsStats: map[string]*SizeAndEntriesCount{
"set": {
EntriesCount: 5,
VolumeInBytes: 6,
},
},
},
},
},
}
expected := map[time.Time]map[string]map[string]*AccumulativeStatsCounter{
time.Date(2022, time.Month(1), 1, 10, 00, 00, 0, time.UTC): {
"http": {
"get": {
Name: "get",
EntriesCount: 1,
VolumeSizeBytes: 2,
},
},
"kafka": {
"listTopics": {
Name: "listTopics",
EntriesCount: 5,
VolumeSizeBytes: 6,
},
},
},
time.Date(2022, time.Month(1), 1, 10, 01, 00, 0, time.UTC): {
"http": {
"post": {
Name: "post",
EntriesCount: 2,
VolumeSizeBytes: 3,
},
"get": {
Name: "get",
EntriesCount: 1,
VolumeSizeBytes: 2,
},
},
"redis": {
"set": {
Name: "set",
EntriesCount: 5,
VolumeSizeBytes: 6,
},
},
},
}
actual, _ := getAggregatedResultTimingFromSpecificTime(60, bucketStatsForTest, time.Date(2022, time.Month(1), 1, 10, 00, 00, 0, time.UTC))
if !reflect.DeepEqual(actual, expected) {
t.Errorf("unexpected result - expected: %v, actual: %v", 3, len(actual))
}
}

View File

@@ -81,5 +81,4 @@ func TestEntryAddedVolume(t *testing.T) {
} }
}) })
} }
} }

View File

@@ -16,7 +16,8 @@ func StatusRoutes(ginApp *gin.Engine) {
routeGroup.GET("/tap", controllers.GetTappingStatus) routeGroup.GET("/tap", controllers.GetTappingStatus)
routeGroup.GET("/general", controllers.GetGeneralStats) // get general stats about entries in DB routeGroup.GET("/general", controllers.GetGeneralStats) // get general stats about entries in DB
routeGroup.GET("/accumulative", controllers.GetAccumulativeStats) // get general stats about entries in DB routeGroup.GET("/accumulative", controllers.GetAccumulativeStats)
routeGroup.GET("/accumulativeTiming", controllers.GetAccumulativeStatsTiming)
routeGroup.GET("/resolving", controllers.GetCurrentResolvingInformation) routeGroup.GET("/resolving", controllers.GetCurrentResolvingInformation)
} }

View File

@@ -20,7 +20,7 @@
"test:lint": "eslint .", "test:lint": "eslint .",
"predeploy": "cd example && npm install && npm run build", "predeploy": "cd example && npm install && npm run build",
"deploy": "gh-pages -d example/build", "deploy": "gh-pages -d example/build",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx" "eslint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0"
}, },
"peerDependencies": { "peerDependencies": {
"@craco/craco": "^6.4.3", "@craco/craco": "^6.4.3",

View File

@@ -109,7 +109,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({
if (scrollTo) { if (scrollTo) {
scrollableRef.current.scrollToIndex(data.data.length - 1); scrollableRef.current.scrollToIndex(data.data.length - 1);
} }
}, [setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, setQueriedTotal, setTruncatedTimestamp, scrollableRef]); }, [setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, setQueriedTotal, setTruncatedTimestamp, scrollableRef, trafficViewerApi]);
useEffect(() => { useEffect(() => {
if (!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return; if (!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return;
@@ -121,7 +121,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({
useEffect(() => { useEffect(() => {
if (!focusedEntryId && entries.length > 0) if (!focusedEntryId && entries.length > 0)
setFocusedEntryId(entries[0].id); setFocusedEntryId(entries[0].id);
}, [focusedEntryId, entries]) }, [focusedEntryId, entries, setFocusedEntryId])
useEffect(() => { useEffect(() => {
const newEntries = [...entries]; const newEntries = [...entries];
@@ -131,7 +131,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({
setNoMoreDataTop(false); setNoMoreDataTop(false);
setEntries(newEntries); setEntries(newEntries);
} }
}, [entries]) }, [entries, setLeftOffTop, setNoMoreDataTop, setEntries])
if(ws.current && !ws.current.onmessage) { if(ws.current && !ws.current.onmessage) {
ws.current.onmessage = (e) => { ws.current.onmessage = (e) => {

View File

@@ -15,7 +15,6 @@ import outgoingIconFailure from "assets/outgoing-traffic-failure.svg"
import outgoingIconNeutral from "assets/outgoing-traffic-neutral.svg" import outgoingIconNeutral from "assets/outgoing-traffic-neutral.svg"
import {useRecoilState} from "recoil"; import {useRecoilState} from "recoil";
import focusedEntryIdAtom from "../../recoil/focusedEntryId"; import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import queryAtom from "../../recoil/query";
interface TCPInterface { interface TCPInterface {
ip: string ip: string
@@ -66,7 +65,6 @@ enum CaptureTypes {
export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode, namespace}) => { export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode, namespace}) => {
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const [queryState, setQuery] = useRecoilState(queryAtom);
const isSelected = focusedEntryId === entry.id; const isSelected = focusedEntryId === entry.id;
const classification = getClassification(entry.status) const classification = getClassification(entry.status)

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react"; import React, {useCallback, useEffect, useRef, useState} from "react";
import { Filters } from "../Filters/Filters"; import { Filters } from "../Filters/Filters";
import { EntriesList } from "../EntriesList/EntriesList"; import { EntriesList } from "../EntriesList/EntriesList";
import makeStyles from '@mui/styles/makeStyles'; import makeStyles from '@mui/styles/makeStyles';
@@ -59,7 +59,6 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({
}) => { }) => {
const classes = useLayoutStyles(); const classes = useLayoutStyles();
const setEntries = useSetRecoilState(entriesAtom); const setEntries = useSetRecoilState(entriesAtom);
const setFocusedEntryId = useSetRecoilState(focusedEntryIdAtom); const setFocusedEntryId = useSetRecoilState(focusedEntryIdAtom);
const query = useRecoilValue(queryAtom); const query = useRecoilValue(queryAtom);
@@ -68,40 +67,44 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({
const [noMoreDataTop, setNoMoreDataTop] = useState(false); const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [isSnappedToBottom, setIsSnappedToBottom] = useState(true); const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
const [wsReadyState, setWsReadyState] = useState(0); const [wsReadyState, setWsReadyState] = useState(0);
const setLeftOffTop = useSetRecoilState(leftOffTopAtom); const setLeftOffTop = useSetRecoilState(leftOffTopAtom);
const scrollableRef = useRef(null); const scrollableRef = useRef(null);
const ws = useRef(null);
const closeWebSocket = useCallback(() => {
if (ws?.current?.readyState === WebSocket.OPEN) {
ws.current.close();
return true;
}
}, [])
useEffect(() => { useEffect(() => {
if(shouldCloseWebSocket){ if(shouldCloseWebSocket){
closeWebSocket() closeWebSocket()
setShouldCloseWebSocket(false); setShouldCloseWebSocket(false);
} }
}, [shouldCloseWebSocket]) }, [shouldCloseWebSocket, setShouldCloseWebSocket, closeWebSocket])
useEffect(() => { const sendQueryWhenWsOpen = useCallback((leftOff: string, query: string, fetch: number, fetchTimeoutMs: number) => {
reopenConnection() setTimeout(() => {
}, [webSocketUrl]) if (ws?.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({
const ws = useRef(null); "leftOff": leftOff,
"query": query,
const openEmptyWebSocket = () => { "enableFullEntries": false,
openWebSocket(DEFAULT_LEFTOFF, query, true, DEFAULT_FETCH, DEFAULT_FETCH_TIMEOUT_MS); "fetch": fetch,
} "timeoutMs": fetchTimeoutMs
}));
const closeWebSocket = () => { } else {
if (ws?.current?.readyState === WebSocket.OPEN) { sendQueryWhenWsOpen(leftOff, query, fetch, fetchTimeoutMs);
ws.current.close(); }
return true; }, 500)
} }, [])
}
const listEntry = useRef(null); const listEntry = useRef(null);
const openWebSocket = (leftOff: string, query: string, resetEntries: boolean, fetch: number, fetchTimeoutMs: number) => { const openWebSocket = useCallback((leftOff: string, query: string, resetEntries: boolean, fetch: number, fetchTimeoutMs: number) => {
if (resetEntries) { if (resetEntries) {
setFocusedEntryId(null); setFocusedEntryId(null);
setEntries([]); setEntries([]);
@@ -127,24 +130,11 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({
} }
} catch (e) { } catch (e) {
} }
} }, [setFocusedEntryId, setEntries, setLeftOffTop, setNoMoreDataTop, ws, sendQueryWhenWsOpen, webSocketUrl])
const sendQueryWhenWsOpen = (leftOff: string, query: string, fetch: number, fetchTimeoutMs: number) => {
setTimeout(() => {
if (ws?.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({
"leftOff": leftOff,
"query": query,
"enableFullEntries": false,
"fetch": fetch,
"timeoutMs": fetchTimeoutMs
}));
} else {
sendQueryWhenWsOpen(leftOff, query, fetch, fetchTimeoutMs);
}
}, 500)
}
const openEmptyWebSocket = useCallback(() => {
openWebSocket(DEFAULT_LEFTOFF, query, true, DEFAULT_FETCH, DEFAULT_FETCH_TIMEOUT_MS);
}, [openWebSocket, query])
useEffect(() => { useEffect(() => {
setTrafficViewerApiState({...trafficViewerApiProp, webSocket: {close: closeWebSocket}}); setTrafficViewerApiState({...trafficViewerApiProp, webSocket: {close: closeWebSocket}});
@@ -156,7 +146,7 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({
console.error(error); console.error(error);
} }
})() })()
}, []); }, [trafficViewerApiProp, closeWebSocket, setTappingStatus, setTrafficViewerApiState]);
const toggleConnection = () => { const toggleConnection = () => {
if (!closeWebSocket()) { if (!closeWebSocket()) {
@@ -166,12 +156,17 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({
} }
} }
const reopenConnection = async () => { const reopenConnection = useCallback(async () => {
closeWebSocket() closeWebSocket()
openEmptyWebSocket(); openEmptyWebSocket();
scrollableRef.current.jumpToBottom(); scrollableRef.current.jumpToBottom();
setIsSnappedToBottom(true); setIsSnappedToBottom(true);
} }, [scrollableRef, setIsSnappedToBottom, closeWebSocket, openEmptyWebSocket])
useEffect(() => {
reopenConnection()
// eslint-disable-next-line
}, [webSocketUrl])
useEffect(() => { useEffect(() => {
return () => { return () => {

View File

@@ -11,7 +11,7 @@ interface LinkProps {
} }
export const Link: React.FC<LinkProps> = ({ link, className, title, children }) => { export const Link: React.FC<LinkProps> = ({ link, className, title, children }) => {
return <a href={link} className={className} title={title} target="_blank"> return <a href={link} className={className} title={title} target="_blank" rel="noreferrer">
{children} {children}
</a> </a>
} }

View File

@@ -66,7 +66,7 @@ const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], mul
} }
setCheckedValues(newChecked) setCheckedValues(newChecked)
}, [searchValue, checkedValues, filteredValuesKeys]) }, [checkedValues, filteredValuesKeys, items, setCheckedValues])
const dataFieldFunc = (listValue) => listValue.component ? listValue.component : const dataFieldFunc = (listValue) => listValue.component ? listValue.component :
<span className={styles.nowrap} title={listValue.value}> <span className={styles.nowrap} title={listValue.value}>

View File

@@ -0,0 +1,4 @@
.barChartContainer
width: 100%
display: flex
justify-content: center

View File

@@ -0,0 +1,83 @@
import styles from "./TimelineBarChart.module.sass";
import { StatsMode } from "../TrafficStatsModal"
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
Legend
} from "recharts";
import { Utils } from "../../../../helpers/Utils";
interface TimelineBarChartProps {
timeLineBarChartMode: string;
data: any;
}
export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarChartMode, data }) => {
const [protocolStats, setProtocolStats] = useState([]);
const [protocolsNamesAndColors, setProtocolsNamesAndColors] = useState([]);
const padTo2Digits = useCallback((num) => {
return String(num).padStart(2, '0');
}, [])
const getHoursAndMinutes = useCallback((protocolTimeKey) => {
const time = new Date(protocolTimeKey)
const hoursAndMinutes = padTo2Digits(time.getHours()) + ':' + padTo2Digits(time.getMinutes());
return hoursAndMinutes;
}, [padTo2Digits])
const creatUniqueObjArray = useCallback((objArray) => {
return [
...new Map(objArray.map((item) => [item["name"], item])).values(),
];
}, [])
useEffect(() => {
if (!data) return;
const protocolsBarsData = [];
const prtcNames = [];
data.forEach(protocolObj => {
let obj: { [k: string]: any } = {};
obj.timestamp = getHoursAndMinutes(protocolObj.timestamp);
protocolObj.protocols.forEach(protocol => {
obj[`${protocol.name}`] = protocol[StatsMode[timeLineBarChartMode]];
prtcNames.push({ name: protocol.name, color: protocol.color });
})
protocolsBarsData.push(obj);
})
const uniqueObjArray = creatUniqueObjArray(prtcNames);
protocolsBarsData.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1);
setProtocolStats(protocolsBarsData);
setProtocolsNamesAndColors(uniqueObjArray);
}, [data, timeLineBarChartMode, setProtocolStats, setProtocolsNamesAndColors, creatUniqueObjArray, getHoursAndMinutes])
const bars = useMemo(() => protocolsNamesAndColors.map((protocolToDIsplay) => {
return <Bar key={protocolToDIsplay.name} dataKey={protocolToDIsplay.name} stackId="a" fill={protocolToDIsplay.color} />
}), [protocolsNamesAndColors])
return (
<div className={styles.barChartContainer}>
<BarChart
width={730}
height={250}
data={protocolStats}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 5
}}
>
<XAxis dataKey="timestamp" />
<YAxis tickFormatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value} />
<Tooltip formatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"} />
<Legend />
{bars}
</BarChart>
</div>
);
}

View File

@@ -2,11 +2,7 @@ import React, {useEffect, useMemo, useState} from "react";
import styles from "./TrafficPieChart.module.sass"; import styles from "./TrafficPieChart.module.sass";
import {Cell, Legend, Pie, PieChart, Tooltip} from "recharts"; import {Cell, Legend, Pie, PieChart, Tooltip} from "recharts";
import {Utils} from "../../../../helpers/Utils"; import {Utils} from "../../../../helpers/Utils";
import {StatsMode as PieChartMode} from "../TrafficStatsModal"
enum PieChartMode {
REQUESTS = "entriesCount",
VOLUME = "volumeSizeBytes"
}
const COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000']; const COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000'];
@@ -24,6 +20,8 @@ const renderCustomizedLabel = ({
const x = cx + radius * Math.cos(-midAngle * RADIAN); const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN); const y = cy + radius * Math.sin(-midAngle * RADIAN);
if (Number((percent * 100).toFixed(0)) <= 1) return;
return ( return (
<text <text
x={x} x={x}

View File

@@ -1,8 +1,9 @@
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {Backdrop, Box, Fade, Modal} from "@mui/material"; import { Backdrop, Box, Fade, Modal } from "@mui/material";
import styles from "./TrafficStatsModal.module.sass"; import styles from "./TrafficStatsModal.module.sass";
import closeIcon from "assets/close.svg"; import closeIcon from "assets/close.svg";
import {TrafficPieChart} from "./TrafficPieChart/TrafficPieChart"; import { TrafficPieChart } from "./TrafficPieChart/TrafficPieChart";
import { TimelineBarChart } from "./TimelineBarChart/TimelineBarChart";
import spinnerImg from "assets/spinner.svg"; import spinnerImg from "assets/spinner.svg";
const modalStyle = { const modalStyle = {
@@ -19,7 +20,7 @@ const modalStyle = {
color: '#000', color: '#000',
}; };
enum StatsMode { export enum StatsMode {
REQUESTS = "entriesCount", REQUESTS = "entriesCount",
VOLUME = "volumeSizeBytes" VOLUME = "volumeSizeBytes"
} }
@@ -27,23 +28,27 @@ enum StatsMode {
interface TrafficStatsModalProps { interface TrafficStatsModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
getTrafficStatsDataApi: () => Promise<any> getPieStatsDataApi: () => Promise<any>
getTimelineStatsDataApi: () => Promise<any>
} }
export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, onClose, getTrafficStatsDataApi }) => { export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, onClose, getPieStatsDataApi, getTimelineStatsDataApi }) => {
const modes = Object.keys(StatsMode).filter(x => !(parseInt(x) >= 0)); const modes = Object.keys(StatsMode).filter(x => !(parseInt(x) >= 0));
const [statsMode, setStatsMode] = useState(modes[0]); const [statsMode, setStatsMode] = useState(modes[0]);
const [statsData, setStatsData] = useState(null); const [pieStatsData, setPieStatsData] = useState(null);
const [timelineStatsData, setTimelineStatsData] = useState(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
if(isOpen && getTrafficStatsDataApi) { if (isOpen && getPieStatsDataApi) {
(async () => { (async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const data = await getTrafficStatsDataApi(); const pieData = await getPieStatsDataApi();
setStatsData(data); setPieStatsData(pieData);
const timelineData = await getTimelineStatsDataApi();
setTimelineStatsData(timelineData);
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {
@@ -51,7 +56,7 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
} }
})() })()
} }
}, [isOpen, getTrafficStatsDataApi]) }, [isOpen, getPieStatsDataApi, getTimelineStatsDataApi, setPieStatsData, setTimelineStatsData])
return ( return (
<Modal <Modal
@@ -65,19 +70,25 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
<Fade in={isOpen}> <Fade in={isOpen}>
<Box sx={modalStyle}> <Box sx={modalStyle}>
<div className={styles.closeIcon}> <div className={styles.closeIcon}>
<img src={closeIcon} alt="close" onClick={() => onClose()} style={{ cursor: "pointer", userSelect: "none" }}/> <img src={closeIcon} alt="close" onClick={() => onClose()} style={{ cursor: "pointer", userSelect: "none" }} />
</div> </div>
<div className={styles.title}>Traffic Statistics</div> <div className={styles.title}>Traffic Statistics</div>
<div className={styles.mainContainer}> <div className={styles.mainContainer}>
<div> <div>
<span style={{marginRight: 15}}>Breakdown By</span> <span style={{ marginRight: 15 }}>Breakdown By</span>
<select className={styles.select} value={statsMode} onChange={(e) => setStatsMode(e.target.value)}> <select className={styles.select} value={statsMode} onChange={(e) => setStatsMode(e.target.value)}>
{modes.map(mode => <option value={mode}>{mode}</option>)} {modes.map(mode => <option key={mode} value={mode}>{mode}</option>)}
</select> </select>
</div> </div>
{isLoading ? <div style={{textAlign: "center", marginTop: 20}}> <div>
{isLoading ? <div style={{ textAlign: "center", marginTop: 20 }}>
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} /> <img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
</div> : <TrafficPieChart pieChartMode={statsMode} data={statsData}/>} </div> :
<div>
<TrafficPieChart pieChartMode={statsMode} data={pieStatsData} />
<TimelineBarChart timeLineBarChartMode={statsMode} data={timelineStatsData} />
</div>}
</div>
</div> </div>
</Box> </Box>
</Fade> </Fade>

View File

@@ -1,5 +1,4 @@
import { atom } from "recoil"; import { atom } from "recoil";
import TrafficViewerApi from "../../components/TrafficViewer/TrafficViewerApi";
const TrafficViewerApiAtom = atom({ const TrafficViewerApiAtom = atom({
key: "TrafficViewerApiAtom", key: "TrafficViewerApiAtom",

View File

@@ -36,7 +36,7 @@ const App = () => {
openModal={oasModalOpen} openModal={oasModalOpen}
handleCloseModal={() => setOasModalOpen(false)} handleCloseModal={() => setOasModalOpen(false)}
/>} />}
<TrafficStatsModal isOpen={trafficStatsModalOpen} onClose={() => setTrafficStatsModalOpen(false)} getTrafficStatsDataApi={api.getStats}/> <TrafficStatsModal isOpen={trafficStatsModalOpen} onClose={() => setTrafficStatsModalOpen(false)} getPieStatsDataApi={api.getPieStats} getTimelineStatsDataApi={api.getTimelineStats}/>
</div> </div>
</ThemeProvider> </ThemeProvider>
</StyledEngineProvider> </StyledEngineProvider>

View File

@@ -111,8 +111,13 @@ export default class Api {
}); });
} }
getStats = async () => { getPieStats = async () => {
const response = await client.get("/status/accumulative"); const response = await client.get("/status/accumulative");
return response.data; return response.data;
} }
getTimelineStats = async () => {
const response = await client.get("/status/accumulativeTiming");
return response.data;
}
} }