Compare commits

...

12 Commits

Author SHA1 Message Date
RamiBerm
76a6a77a14 Refactor ws (#961)
* Separate socket and basenine logic

* WIP

* Update socket_server_handlers.go

* Update socket_data_streamer.go and socket_server_handlers.go

* Update socket_server_handlers.go

* Merge branch 'develop' into refactor_ws
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

* empty commit for actions

* empty commit for actions

* commit for actions

* Revert "commit for actions"

This reverts commit 8ba2ecf7d3.

Co-authored-by: RoyUP9 <87927115+RoyUP9@users.noreply.github.com>
2022-04-04 17:33:53 +03:00
M. Mert Yıldıran
2bfc523bbc Handle reflect.TypeOf returning nil case (#970) 2022-04-04 16:25:18 +03:00
RoyUP9
66ba778384 Fixed golint modified files (#969) 2022-04-04 15:32:22 +03:00
leon-up9
7adbf7bf1b Ui/TRA-4461_service-map-&-OAS---GUI-changes (#962)
* OpenAPI renamed to Service Catalog

* Docs icon change
Hide Navbar on serviceMap modal open

* PR comments

Co-authored-by: Leon <>
Co-authored-by: RoyUP9 <87927115+RoyUP9@users.noreply.github.com>
2022-04-04 14:49:41 +03:00
Igor Gov
a97b5b3b38 Add conditional Go lint validation to CI (#967) 2022-04-04 14:35:47 +03:00
Nimrod Gilboa Markevich
aa8dcc5f5c Format commit message as code to handle multi line messages (#963)
Co-authored-by: gadotroee <55343099+gadotroee@users.noreply.github.com>
2022-04-03 22:10:43 +03:00
lirazyehezkel
9d08dbdd5d UI performance fix 2022-04-03 21:35:09 +03:00
lirazyehezkel
b47718e094 TRA-4442 Improve UI performance (#960)
* Move ws entry listener to entriesList component

* unused code
2022-04-03 15:51:20 +03:00
Igor Gov
6a7fad430c Adding resolved prop to service map node (#959)
* Adding resolved prop to service map node

* fixing tests
2022-04-03 15:32:21 +03:00
lirazyehezkel
59ad8d8fad TLS icon position (#956)
* TLS icon position

* cr fix
2022-03-31 11:26:58 +03:00
Nimrod Gilboa Markevich
a49443f101 Set the entry namespace to the source namespace if the destination is not resolved (#950)
* Set the entry namespace to the source namespace if the destination is not resolved
* Overwrite src namespace with dst namespace only if dst non-empty
2022-03-30 15:40:21 +03:00
lirazyehezkel
2427955aa4 Avoid overlap only for service map including under 10 services 2022-03-30 15:30:09 +03:00
28 changed files with 462 additions and 335 deletions

View File

@@ -43,7 +43,7 @@ jobs:
with: with:
status: ${{ job.status }} status: ${{ job.status }}
notification_title: 'Mizu {workflow} has {status_message}' notification_title: 'Mizu {workflow} has {status_message}'
message_format: '{emoji} *{workflow}* {status_message} during <{run_url}|run>, after commit <{commit_url}|{commit_sha} ${{ github.event.head_commit.message }}> ${{ github.event.head_commit.committer.name }} <${{ github.event.head_commit.committer.email }}>' message_format: '{emoji} *{workflow}* {status_message} during <{run_url}|run>, after commit <{commit_url}|{commit_sha}> by ${{ github.event.head_commit.committer.name }} <${{ github.event.head_commit.committer.email }}> ```${{ github.event.head_commit.message }}```'
footer: 'Linked Repo <{repo_url}|{repo}>' footer: 'Linked Repo <{repo_url}|{repo}>'
notify_when: 'failure' notify_when: 'failure'
env: env:

View File

@@ -15,6 +15,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
fetch-depth: 2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: '^1.17' go-version: '^1.17'
@@ -24,67 +27,117 @@ jobs:
sudo apt update sudo apt update
sudo apt install -y libpcap-dev sudo apt install -y libpcap-dev
- name: Check Agent modified files
id: agent_modified_files
run: devops/check_modified_files.sh agent/
- name: Go lint - agent - name: Go lint - agent
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.agent_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: agent working-directory: agent
args: --timeout=3m args: --timeout=3m
- name: Check shared modified files
id: shared_modified_files
run: devops/check_modified_files.sh shared/
- name: Go lint - shared - name: Go lint - shared
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.shared_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: shared working-directory: shared
args: --timeout=3m args: --timeout=3m
- name: Check tap modified files
id: tap_modified_files
run: devops/check_modified_files.sh tap/
- name: Go lint - tap - name: Go lint - tap
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.tap_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: tap working-directory: tap
args: --timeout=3m args: --timeout=3m
- name: Check cli modified files
id: cli_modified_files
run: devops/check_modified_files.sh cli/
- name: Go lint - CLI - name: Go lint - CLI
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.cli_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: cli working-directory: cli
args: --timeout=3m args: --timeout=3m
- name: Check acceptanceTests modified files
id: acceptanceTests_modified_files
run: devops/check_modified_files.sh acceptanceTests/
- name: Go lint - acceptanceTests - name: Go lint - acceptanceTests
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.acceptanceTests_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: acceptanceTests working-directory: acceptanceTests
args: --timeout=3m args: --timeout=3m
- name: Check tap/api modified files
id: tap_api_modified_files
run: devops/check_modified_files.sh tap/api/
- name: Go lint - tap/api - name: Go lint - tap/api
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.tap_api_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: tap/api working-directory: tap/api
- name: Check tap/extensions/amqp modified files
id: tap_amqp_modified_files
run: devops/check_modified_files.sh tap/extensions/amqp/
- name: Go lint - tap/extensions/amqp - name: Go lint - tap/extensions/amqp
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.tap_amqp_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: tap/extensions/amqp working-directory: tap/extensions/amqp
- name: Check tap/extensions/http modified files
id: tap_http_modified_files
run: devops/check_modified_files.sh tap/extensions/http/
- name: Go lint - tap/extensions/http - name: Go lint - tap/extensions/http
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.tap_http_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: tap/extensions/http working-directory: tap/extensions/http
- name: Check tap/extensions/kafka modified files
id: tap_kafka_modified_files
run: devops/check_modified_files.sh tap/extensions/kafka/
- name: Go lint - tap/extensions/kafka - name: Go lint - tap/extensions/kafka
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.tap_kafka_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: tap/extensions/kafka working-directory: tap/extensions/kafka
- name: Check tap/extensions/redis modified files
id: tap_redis_modified_files
run: devops/check_modified_files.sh tap/extensions/redis/
- name: Go lint - tap/extensions/redis - name: Go lint - tap/extensions/redis
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v2
if: steps.tap_redis_modified_files.outputs.matched == 'true'
with: with:
version: latest version: latest
working-directory: tap/extensions/redis working-directory: tap/extensions/redis

View File

@@ -373,4 +373,6 @@ func initializeDependencies() {
dependency.RegisterGenerator(dependency.ServiceMapGeneratorDependency, func() interface{} { return servicemap.GetDefaultServiceMapInstance() }) dependency.RegisterGenerator(dependency.ServiceMapGeneratorDependency, func() interface{} { return servicemap.GetDefaultServiceMapInstance() })
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance(nil) }) dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance(nil) })
dependency.RegisterGenerator(dependency.EntriesProvider, func() interface{} { return &entries.BasenineEntriesProvider{} }) dependency.RegisterGenerator(dependency.EntriesProvider, func() interface{} { return &entries.BasenineEntriesProvider{} })
dependency.RegisterGenerator(dependency.EntriesSocketStreamer, func() interface{} { return &api.BasenineEntryStreamer{} })
dependency.RegisterGenerator(dependency.EntryStreamerSocketConnector, func() interface{} { return &api.DefaultEntryStreamerSocketConnector{} })
} }

View File

@@ -0,0 +1,57 @@
package api
import (
"fmt"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
)
type EntryStreamerSocketConnector interface {
SendEntry(socketId int, entry *tapApi.Entry, params *WebSocketParams)
SendMetadata(socketId int, metadata *basenine.Metadata)
SendToastError(socketId int, err error)
CleanupSocket(socketId int)
}
type DefaultEntryStreamerSocketConnector struct{}
func (e *DefaultEntryStreamerSocketConnector) SendEntry(socketId int, entry *tapApi.Entry, params *WebSocketParams) {
var message []byte
if params.EnableFullEntries {
message, _ = models.CreateFullEntryWebSocketMessage(entry)
} else {
extension := extensionsMap[entry.Protocol.Name]
base := extension.Dissector.Summarize(entry)
message, _ = models.CreateBaseEntryWebSocketMessage(base)
}
if err := SendToSocket(socketId, message); err != nil {
logger.Log.Error(err)
}
}
func (e *DefaultEntryStreamerSocketConnector) SendMetadata(socketId int, metadata *basenine.Metadata) {
metadataBytes, _ := models.CreateWebsocketQueryMetadataMessage(metadata)
if err := SendToSocket(socketId, metadataBytes); err != nil {
logger.Log.Error(err)
}
}
func (e *DefaultEntryStreamerSocketConnector) SendToastError(socketId int, err error) {
toastBytes, _ := models.CreateWebsocketToastMessage(&models.ToastMessage{
Type: "error",
AutoClose: 5000,
Text: fmt.Sprintf("Syntax error: %s", err.Error()),
})
if err := SendToSocket(socketId, toastBytes); err != nil {
logger.Log.Error(err)
}
}
func (e *DefaultEntryStreamerSocketConnector) CleanupSocket(socketId int) {
socketObj := connectedWebsockets[socketId]
socketCleanup(socketId, socketObj)
}

View File

@@ -168,6 +168,7 @@ func resolveIP(connectionInfo *tapApi.ConnectionInfo) (resolvedSource string, re
} }
} else { } else {
resolvedSource = resolvedSourceObject.FullAddress resolvedSource = resolvedSourceObject.FullAddress
namespace = resolvedSourceObject.Namespace
} }
unresolvedDestination := fmt.Sprintf("%s:%s", connectionInfo.ServerIP, connectionInfo.ServerPort) unresolvedDestination := fmt.Sprintf("%s:%s", connectionInfo.ServerIP, connectionInfo.ServerPort)
@@ -179,7 +180,11 @@ func resolveIP(connectionInfo *tapApi.ConnectionInfo) (resolvedSource string, re
} }
} else { } else {
resolvedDestination = resolvedDestinationObject.FullAddress resolvedDestination = resolvedDestinationObject.FullAddress
namespace = resolvedDestinationObject.Namespace // Overwrite namespace (if it was set according to the source)
// Only overwrite if non-empty
if resolvedDestinationObject.Namespace != "" {
namespace = resolvedDestinationObject.Namespace
}
} }
} }
return resolvedSource, resolvedDestination, namespace return resolvedSource, resolvedDestination, namespace

View File

@@ -0,0 +1,92 @@
package api
import (
"context"
"encoding/json"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/agent/pkg/dependency"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
)
type EntryStreamer interface {
Get(ctx context.Context, socketId int, params *WebSocketParams) error
}
type BasenineEntryStreamer struct{}
func (e *BasenineEntryStreamer) Get(ctx context.Context, socketId int, params *WebSocketParams) error {
var connection *basenine.Connection
entryStreamerSocketConnector := dependency.GetInstance(dependency.EntryStreamerSocketConnector).(EntryStreamerSocketConnector)
connection, err := basenine.NewConnection(shared.BasenineHost, shared.BaseninePort)
if err != nil {
logger.Log.Errorf("failed to establish a connection to Basenine: %v", err)
entryStreamerSocketConnector.CleanupSocket(socketId)
return err
}
data := make(chan []byte)
meta := make(chan []byte)
query := params.Query
err = basenine.Validate(shared.BasenineHost, shared.BaseninePort, query)
if err != nil {
entryStreamerSocketConnector.SendToastError(socketId, err)
}
handleDataChannel := func(c *basenine.Connection, data chan []byte) {
for {
bytes := <-data
if string(bytes) == basenine.CloseChannel {
return
}
var entry *tapApi.Entry
err = json.Unmarshal(bytes, &entry)
if err != nil {
logger.Log.Debugf("error unmarshalling entry: %v", err.Error())
continue
}
entryStreamerSocketConnector.SendEntry(socketId, entry, params)
}
}
handleMetaChannel := func(c *basenine.Connection, meta chan []byte) {
for {
bytes := <-meta
if string(bytes) == basenine.CloseChannel {
return
}
var metadata *basenine.Metadata
err = json.Unmarshal(bytes, &metadata)
if err != nil {
logger.Log.Debugf("Error unmarshalling metadata: %v", err.Error())
continue
}
entryStreamerSocketConnector.SendMetadata(socketId, metadata)
}
}
go handleDataChannel(connection, data)
go handleMetaChannel(connection, meta)
connection.Query(query, data, meta)
go func() {
<-ctx.Done()
data <- []byte(basenine.CloseChannel)
meta <- []byte(basenine.CloseChannel)
connection.Close()
}()
return nil
}

View File

@@ -1,19 +1,15 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sync" "sync"
"time" "time"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
basenine "github.com/up9inc/basenine/client/go" "github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/shared" "github.com/up9inc/mizu/agent/pkg/utils"
"github.com/up9inc/mizu/shared/logger" "github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api" tapApi "github.com/up9inc/mizu/tap/api"
) )
@@ -25,9 +21,9 @@ func InitExtensionsMap(ref map[string]*tapApi.Extension) {
} }
type EventHandlers interface { type EventHandlers interface {
WebSocketConnect(socketId int, isTapper bool) WebSocketConnect(c *gin.Context, socketId int, isTapper bool)
WebSocketDisconnect(socketId int, isTapper bool) WebSocketDisconnect(socketId int, isTapper bool)
WebSocketMessage(socketId int, message []byte) WebSocketMessage(socketId int, isTapper bool, message []byte)
} }
type SocketConnection struct { type SocketConnection struct {
@@ -62,11 +58,11 @@ func init() {
func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) { func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) {
SocketGetBrowserHandler = func(c *gin.Context) { SocketGetBrowserHandler = func(c *gin.Context) {
websocketHandler(c.Writer, c.Request, eventHandlers, false) websocketHandler(c, eventHandlers, false)
} }
SocketGetTapperHandler = func(c *gin.Context) { SocketGetTapperHandler = func(c *gin.Context) {
websocketHandler(c.Writer, c.Request, eventHandlers, true) websocketHandler(c, eventHandlers, true)
} }
app.GET("/ws", func(c *gin.Context) { app.GET("/ws", func(c *gin.Context) {
@@ -78,10 +74,10 @@ func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) {
}) })
} }
func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers EventHandlers, isTapper bool) { func websocketHandler(c *gin.Context, eventHandlers EventHandlers, isTapper bool) {
ws, err := websocketUpgrader.Upgrade(w, r, nil) ws, err := websocketUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
logger.Log.Errorf("Failed to set websocket upgrade: %v", err) logger.Log.Errorf("failed to set websocket upgrade: %v", err)
return return
} }
@@ -93,30 +89,11 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
websocketIdsLock.Unlock() websocketIdsLock.Unlock()
var connection *basenine.Connection
var isQuerySet bool
// `!isTapper` means it's a connection from the web UI
if !isTapper {
connection, err = basenine.NewConnection(shared.BasenineHost, shared.BaseninePort)
if err != nil {
logger.Log.Errorf("Failed to establish a connection to Basenine: %v", err)
socketCleanup(socketId, connectedWebsockets[socketId])
return
}
}
data := make(chan []byte)
meta := make(chan []byte)
defer func() { defer func() {
socketCleanup(socketId, connectedWebsockets[socketId]) socketCleanup(socketId, connectedWebsockets[socketId])
data <- []byte(basenine.CloseChannel)
meta <- []byte(basenine.CloseChannel)
connection.Close()
}() }()
eventHandlers.WebSocketConnect(socketId, isTapper) eventHandlers.WebSocketConnect(c, socketId, isTapper)
startTimeBytes, _ := models.CreateWebsocketStartTimeMessage(utils.StartTime) startTimeBytes, _ := models.CreateWebsocketStartTimeMessage(utils.StartTime)
@@ -124,127 +101,32 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
logger.Log.Error(err) logger.Log.Error(err)
} }
var params WebSocketParams
for { for {
_, msg, err := ws.ReadMessage() _, msg, err := ws.ReadMessage()
if err != nil { if err != nil {
if _, ok := err.(*websocket.CloseError); ok { if _, ok := err.(*websocket.CloseError); ok {
logger.Log.Debugf("Received websocket close message, socket id: %d", socketId) logger.Log.Debugf("received websocket close message, socket id: %d", socketId)
} else { } else {
logger.Log.Errorf("Error reading message, socket id: %d, error: %v", socketId, err) logger.Log.Errorf("error reading message, socket id: %d, error: %v", socketId, err)
} }
break break
} }
if !isTapper && !isQuerySet { eventHandlers.WebSocketMessage(socketId, isTapper, msg)
if err := json.Unmarshal(msg, &params); err != nil {
logger.Log.Errorf("Error unmarshalling parameters: %v", socketId, err)
continue
}
query := params.Query
err = basenine.Validate(shared.BasenineHost, shared.BaseninePort, query)
if err != nil {
toastBytes, _ := models.CreateWebsocketToastMessage(&models.ToastMessage{
Type: "error",
AutoClose: 5000,
Text: fmt.Sprintf("Syntax error: %s", err.Error()),
})
if err := SendToSocket(socketId, toastBytes); err != nil {
logger.Log.Error(err)
}
break
}
isQuerySet = true
handleDataChannel := func(c *basenine.Connection, data chan []byte) {
for {
bytes := <-data
if string(bytes) == basenine.CloseChannel {
return
}
var entry *tapApi.Entry
err = json.Unmarshal(bytes, &entry)
if err != nil {
logger.Log.Debugf("Error unmarshalling entry: %v", err.Error())
continue
}
var message []byte
if params.EnableFullEntries {
message, _ = models.CreateFullEntryWebSocketMessage(entry)
} else {
extension := extensionsMap[entry.Protocol.Name]
base := extension.Dissector.Summarize(entry)
message, _ = models.CreateBaseEntryWebSocketMessage(base)
}
if err := SendToSocket(socketId, message); err != nil {
logger.Log.Error(err)
}
}
}
handleMetaChannel := func(c *basenine.Connection, meta chan []byte) {
for {
bytes := <-meta
if string(bytes) == basenine.CloseChannel {
return
}
var metadata *basenine.Metadata
err = json.Unmarshal(bytes, &metadata)
if err != nil {
logger.Log.Debugf("Error unmarshalling metadata: %v", err.Error())
continue
}
metadataBytes, _ := models.CreateWebsocketQueryMetadataMessage(metadata)
if err := SendToSocket(socketId, metadataBytes); err != nil {
logger.Log.Error(err)
}
}
}
go handleDataChannel(connection, data)
go handleMetaChannel(connection, meta)
connection.Query(query, data, meta)
} else {
eventHandlers.WebSocketMessage(socketId, msg)
}
} }
} }
func socketCleanup(socketId int, socketConnection *SocketConnection) {
err := socketConnection.connection.Close()
if err != nil {
logger.Log.Errorf("Error closing socket connection for socket id %d: %v", socketId, err)
}
websocketIdsLock.Lock()
connectedWebsockets[socketId] = nil
websocketIdsLock.Unlock()
socketConnection.eventHandlers.WebSocketDisconnect(socketId, socketConnection.isTapper)
}
func SendToSocket(socketId int, message []byte) error { func SendToSocket(socketId int, message []byte) error {
socketObj := connectedWebsockets[socketId] socketObj := connectedWebsockets[socketId]
if socketObj == nil { if socketObj == nil {
return fmt.Errorf("Socket %v is disconnected", socketId) return fmt.Errorf("socket %v is disconnected", socketId)
} }
var sent = false var sent = false
time.AfterFunc(time.Second*5, func() { time.AfterFunc(time.Second*5, func() {
if !sent { if !sent {
logger.Log.Error("Socket timed out") logger.Log.Error("socket timed out")
socketCleanup(socketId, socketObj) socketCleanup(socketId, socketObj)
} }
}) })
@@ -255,7 +137,20 @@ func SendToSocket(socketId int, message []byte) error {
sent = true sent = true
if err != nil { if err != nil {
return fmt.Errorf("Failed to write message to socket %v, err: %w", socketId, err) return fmt.Errorf("failed to write message to socket %v, err: %w", socketId, err)
} }
return nil return nil
} }
func socketCleanup(socketId int, socketConnection *SocketConnection) {
err := socketConnection.connection.Close()
if err != nil {
logger.Log.Errorf("error closing socket connection for socket id %d: %v", socketId, err)
}
websocketIdsLock.Lock()
connectedWebsockets[socketId] = nil
websocketIdsLock.Unlock()
socketConnection.eventHandlers.WebSocketDisconnect(socketId, socketConnection.isTapper)
}

View File

@@ -1,12 +1,13 @@
package api package api
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt"
"sync" "sync"
"github.com/gin-gonic/gin"
"github.com/up9inc/mizu/agent/pkg/dependency"
"github.com/up9inc/mizu/agent/pkg/models" "github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/providers"
"github.com/up9inc/mizu/agent/pkg/providers/tappedPods" "github.com/up9inc/mizu/agent/pkg/providers/tappedPods"
"github.com/up9inc/mizu/agent/pkg/providers/tappers" "github.com/up9inc/mizu/agent/pkg/providers/tappers"
"github.com/up9inc/mizu/agent/pkg/up9" "github.com/up9inc/mizu/agent/pkg/up9"
@@ -17,7 +18,11 @@ import (
"github.com/up9inc/mizu/shared/logger" "github.com/up9inc/mizu/shared/logger"
) )
var browserClientSocketUUIDs = make([]int, 0) type BrowserClient struct {
dataStreamCancelFunc context.CancelFunc
}
var browserClients = make(map[int]*BrowserClient, 0)
var tapperClientSocketUUIDs = make([]int, 0) var tapperClientSocketUUIDs = make([]int, 0)
var socketListLock = sync.Mutex{} var socketListLock = sync.Mutex{}
@@ -30,7 +35,7 @@ func init() {
go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients) go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients)
} }
func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) { func (h *RoutesEventHandlers) WebSocketConnect(_ *gin.Context, socketId int, isTapper bool) {
if isTapper { if isTapper {
logger.Log.Infof("Websocket event - Tapper connected, socket ID: %d", socketId) logger.Log.Infof("Websocket event - Tapper connected, socket ID: %d", socketId)
tappers.Connected() tappers.Connected()
@@ -45,7 +50,7 @@ func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
logger.Log.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId) logger.Log.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId)
socketListLock.Lock() socketListLock.Lock()
browserClientSocketUUIDs = append(browserClientSocketUUIDs, socketId) browserClients[socketId] = &BrowserClient{}
socketListLock.Unlock() socketListLock.Unlock()
BroadcastTappedPodsStatus() BroadcastTappedPodsStatus()
@@ -63,13 +68,16 @@ func (h *RoutesEventHandlers) WebSocketDisconnect(socketId int, isTapper bool) {
} else { } else {
logger.Log.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId) logger.Log.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId)
socketListLock.Lock() socketListLock.Lock()
removeSocketUUIDFromBrowserSlice(socketId) if browserClients[socketId] != nil && browserClients[socketId].dataStreamCancelFunc != nil {
browserClients[socketId].dataStreamCancelFunc()
}
delete(browserClients, socketId)
socketListLock.Unlock() socketListLock.Unlock()
} }
} }
func BroadcastToBrowserClients(message []byte) { func BroadcastToBrowserClients(message []byte) {
for _, socketId := range browserClientSocketUUIDs { for socketId := range browserClients {
go func(socketId int) { go func(socketId int) {
if err := SendToSocket(socketId, message); err != nil { if err := SendToSocket(socketId, message); err != nil {
logger.Log.Error(err) logger.Log.Error(err)
@@ -88,7 +96,33 @@ func BroadcastToTapperClients(message []byte) {
} }
} }
func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) { func (h *RoutesEventHandlers) WebSocketMessage(socketId int, isTapper bool, message []byte) {
if isTapper {
HandleTapperIncomingMessage(message, h.SocketOutChannel, BroadcastToBrowserClients)
} else {
// we initiate the basenine stream after the first websocket message we receive (it contains the entry query), we then store a cancelfunc to later cancel this stream
if browserClients[socketId] != nil && browserClients[socketId].dataStreamCancelFunc == nil {
var params WebSocketParams
if err := json.Unmarshal(message, &params); err != nil {
logger.Log.Errorf("Error: %v", socketId, err)
return
}
entriesStreamer := dependency.GetInstance(dependency.EntriesSocketStreamer).(EntryStreamer)
ctx, cancelFunc := context.WithCancel(context.Background())
err := entriesStreamer.Get(ctx, socketId, &params)
if err != nil {
logger.Log.Errorf("error initializing basenine stream for browser socket %d %+v", socketId, err)
cancelFunc()
} else {
browserClients[socketId].dataStreamCancelFunc = cancelFunc
}
}
}
}
func HandleTapperIncomingMessage(message []byte, socketOutChannel chan<- *tapApi.OutputChannelItem, broadcastMessageFunc func([]byte)) {
var socketMessageBase shared.WebSocketMessageMetadata var socketMessageBase shared.WebSocketMessageMetadata
err := json.Unmarshal(message, &socketMessageBase) err := json.Unmarshal(message, &socketMessageBase)
if err != nil { if err != nil {
@@ -102,7 +136,7 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err) logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else { } else {
// NOTE: This is where the message comes back from the intermediate WebSocket to code. // NOTE: This is where the message comes back from the intermediate WebSocket to code.
h.SocketOutChannel <- tappedEntryMessage.Data socketOutChannel <- tappedEntryMessage.Data
} }
case shared.WebSocketMessageTypeUpdateStatus: case shared.WebSocketMessageTypeUpdateStatus:
var statusMessage shared.WebSocketStatusMessage var statusMessage shared.WebSocketStatusMessage
@@ -110,15 +144,7 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
if err != nil { if err != nil {
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err) logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else { } else {
BroadcastToBrowserClients(message) broadcastMessageFunc(message)
}
case shared.WebsocketMessageTypeOutboundLink:
var outboundLinkMessage models.WebsocketOutboundLinkMessage
err := json.Unmarshal(message, &outboundLinkMessage)
if err != nil {
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else {
handleTLSLink(outboundLinkMessage)
} }
default: default:
logger.Log.Infof("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType) logger.Log.Infof("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType)
@@ -126,39 +152,6 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
} }
} }
func handleTLSLink(outboundLinkMessage models.WebsocketOutboundLinkMessage) {
resolvedNameObject := k8sResolver.Resolve(outboundLinkMessage.Data.DstIP)
if resolvedNameObject != nil {
outboundLinkMessage.Data.DstIP = resolvedNameObject.FullAddress
} else if outboundLinkMessage.Data.SuggestedResolvedName != "" {
outboundLinkMessage.Data.DstIP = outboundLinkMessage.Data.SuggestedResolvedName
}
cacheKey := fmt.Sprintf("%s -> %s:%d", outboundLinkMessage.Data.Src, outboundLinkMessage.Data.DstIP, outboundLinkMessage.Data.DstPort)
_, isInCache := providers.RecentTLSLinks.Get(cacheKey)
if isInCache {
return
} else {
providers.RecentTLSLinks.SetDefault(cacheKey, outboundLinkMessage.Data)
}
marshaledMessage, err := json.Marshal(outboundLinkMessage)
if err != nil {
logger.Log.Errorf("Error marshaling outbound link message for broadcasting: %v", err)
} else {
logger.Log.Errorf("Broadcasting outboundlink message %s", string(marshaledMessage))
BroadcastToBrowserClients(marshaledMessage)
}
}
func removeSocketUUIDFromBrowserSlice(uuidToRemove int) {
newUUIDSlice := make([]int, 0, len(browserClientSocketUUIDs))
for _, uuid := range browserClientSocketUUIDs {
if uuid != uuidToRemove {
newUUIDSlice = append(newUUIDSlice, uuid)
}
}
browserClientSocketUUIDs = newUUIDSlice
}
func removeSocketUUIDFromTapperSlice(uuidToRemove int) { func removeSocketUUIDFromTapperSlice(uuidToRemove int) {
newUUIDSlice := make([]int, 0, len(tapperClientSocketUUIDs)) newUUIDSlice := make([]int, 0, len(tapperClientSocketUUIDs))
for _, uuid := range tapperClientSocketUUIDs { for _, uuid := range tapperClientSocketUUIDs {

View File

@@ -101,16 +101,18 @@ func (s *ServiceMapControllerSuite) TestGet() {
// response nodes // response nodes
aNode := servicemap.ServiceMapNode{ aNode := servicemap.ServiceMapNode{
Id: 1, Id: 1,
Name: TCPEntryA.Name, Name: TCPEntryA.Name,
Entry: TCPEntryA, Entry: TCPEntryA,
Count: 1, Resolved: true,
Count: 1,
} }
bNode := servicemap.ServiceMapNode{ bNode := servicemap.ServiceMapNode{
Id: 2, Id: 2,
Name: TCPEntryB.Name, Name: TCPEntryB.Name,
Entry: TCPEntryB, Entry: TCPEntryB,
Count: 1, Resolved: true,
Count: 1,
} }
assert.Contains(response.Nodes, aNode) assert.Contains(response.Nodes, aNode)
assert.Contains(response.Nodes, bNode) assert.Contains(response.Nodes, bNode)

View File

@@ -6,4 +6,6 @@ const (
ServiceMapGeneratorDependency = "ServiceMapGeneratorDependency" ServiceMapGeneratorDependency = "ServiceMapGeneratorDependency"
OasGeneratorDependency = "OasGeneratorDependency" OasGeneratorDependency = "OasGeneratorDependency"
EntriesProvider = "EntriesProvider" EntriesProvider = "EntriesProvider"
EntriesSocketStreamer = "EntriesSocketStreamer"
EntryStreamerSocketConnector = "EntryStreamerSocketConnector"
) )

View File

@@ -18,10 +18,11 @@ type ServiceMapResponse struct {
} }
type ServiceMapNode struct { type ServiceMapNode struct {
Id int `json:"id"` Id int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Entry *tapApi.TCP `json:"entry"` Entry *tapApi.TCP `json:"entry"`
Count int `json:"count"` Count int `json:"count"`
Resolved bool `json:"resolved"`
} }
type ServiceMapEdge struct { type ServiceMapEdge struct {

View File

@@ -227,10 +227,11 @@ func (s *defaultServiceMap) GetNodes() []ServiceMapNode {
var nodes []ServiceMapNode var nodes []ServiceMapNode
for i, n := range s.graph.Nodes { for i, n := range s.graph.Nodes {
nodes = append(nodes, ServiceMapNode{ nodes = append(nodes, ServiceMapNode{
Id: n.id, Id: n.id,
Name: string(i), Name: string(i),
Entry: n.entry, Resolved: n.entry.Name != UnresolvedNodeName,
Count: n.count, Entry: n.entry,
Count: n.count,
}) })
} }
return nodes return nodes
@@ -243,16 +244,18 @@ func (s *defaultServiceMap) GetEdges() []ServiceMapEdge {
for _, p := range s.graph.Edges[u][v].data { for _, p := range s.graph.Edges[u][v].data {
edges = append(edges, ServiceMapEdge{ edges = append(edges, ServiceMapEdge{
Source: ServiceMapNode{ Source: ServiceMapNode{
Id: s.graph.Nodes[u].id, Id: s.graph.Nodes[u].id,
Name: string(u), Name: string(u),
Entry: s.graph.Nodes[u].entry, Entry: s.graph.Nodes[u].entry,
Count: s.graph.Nodes[u].count, Resolved: s.graph.Nodes[u].entry.Name != UnresolvedNodeName,
Count: s.graph.Nodes[u].count,
}, },
Destination: ServiceMapNode{ Destination: ServiceMapNode{
Id: s.graph.Nodes[v].id, Id: s.graph.Nodes[v].id,
Name: string(v), Name: string(v),
Entry: s.graph.Nodes[v].entry, Entry: s.graph.Nodes[v].entry,
Count: s.graph.Nodes[v].count, Resolved: s.graph.Nodes[v].entry.Name != UnresolvedNodeName,
Count: s.graph.Nodes[v].count,
}, },
Count: p.count, Count: p.count,
Protocol: p.protocol, Protocol: p.protocol,

View File

@@ -53,7 +53,16 @@ func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (re
h := item.(map[string]interface{}) h := item.(map[string]interface{})
key := h["name"].(string) key := h["name"].(string)
value := h["value"] value := h["value"]
switch reflect.TypeOf(value).Kind() {
var reflectKind reflect.Kind
reflectType := reflect.TypeOf(value)
if reflectType == nil {
reflectKind = reflect.Interface
} else {
reflectKind = reflect.TypeOf(value).Kind()
}
switch reflectKind {
case reflect.Slice: case reflect.Slice:
fallthrough fallthrough
case reflect.Array: case reflect.Array:

View File

@@ -42,10 +42,10 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
try { try {
const data = await getOasByService(selectedService ? selectedService : oasServices[0]); const data = await getOasByService(selectedService ? selectedService : oasServices[0]);
setSelectedServiceSpec(data); setSelectedServiceSpec(data);
} catch (e) { } catch (e) {
toast.error("Error occurred while fetching service OAS spec"); toast.error("Error occurred while fetching service OAS spec");
console.error(e); console.error(e);
} }
}; };
useEffect(() => { useEffect(() => {
@@ -61,7 +61,7 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
useEffect(() => { useEffect(() => {
onSelectedOASService(null); onSelectedOASService(null);
},[oasServices]) }, [oasServices])
return ( return (
<Modal <Modal
@@ -80,28 +80,28 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
<div className={style.boxContainer}> <div className={style.boxContainer}>
<div className={style.selectHeader}> <div className={style.selectHeader}>
<div><img src={openApiLogo} alt="openAPI" className={style.openApilogo} /></div> <div><img src={openApiLogo} alt="openAPI" className={style.openApilogo} /></div>
<div className={style.title}>OpenAPI</div> <div className={style.title}>Service Catalog</div>
</div> </div>
<div style={{ cursor: "pointer" }}> <div style={{ cursor: "pointer" }}>
<img src={closeIcon} alt="close" onClick={handleCloseModal} /> <img src={closeIcon} alt="close" onClick={handleCloseModal} />
</div> </div>
</div> </div>
<div className={style.selectContainer} > <div className={style.selectContainer} >
<FormControl> <FormControl>
<Select <Select
labelId="service-select-label" labelId="service-select-label"
id="service-select" id="service-select"
value={selectedServiceName} value={selectedServiceName}
onChangeCb={onSelectedOASService} onChangeCb={onSelectedOASService}
> >
{oasServices.map((service) => ( {oasServices.map((service) => (
<MenuItem key={service} value={service}> <MenuItem key={service} value={service}>
{service} {service}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
<div className={style.borderLine}></div> <div className={style.borderLine}></div>
<div className={style.redoc}> <div className={style.redoc}>
{selectedServiceSpec && <RedocStandalone {selectedServiceSpec && <RedocStandalone

View File

@@ -10,6 +10,7 @@ import entriesAtom from "../../recoil/entries";
import queryAtom from "../../recoil/query"; import queryAtom from "../../recoil/query";
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi"; import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi";
import TrafficViewerApi from "./TrafficViewerApi"; import TrafficViewerApi from "./TrafficViewerApi";
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
interface EntriesListProps { interface EntriesListProps {
listEntryREF: any; listEntryREF: any;
@@ -18,8 +19,6 @@ interface EntriesListProps {
setIsSnappedToBottom: any; setIsSnappedToBottom: any;
queriedCurrent: number; queriedCurrent: number;
setQueriedCurrent: any; setQueriedCurrent: any;
queriedTotal: number;
setQueriedTotal: any;
startTime: number; startTime: number;
noMoreDataTop: boolean; noMoreDataTop: boolean;
setNoMoreDataTop: (flag: boolean) => void; setNoMoreDataTop: (flag: boolean) => void;
@@ -33,16 +32,18 @@ interface EntriesListProps {
ws: any; ws: any;
} }
export const EntriesList: React.FC<EntriesListProps> = ({listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, leftOffTop, setLeftOffTop, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef, ws}) => { export const EntriesList: React.FC<EntriesListProps> = ({listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, startTime, noMoreDataTop, setNoMoreDataTop, leftOffTop, setLeftOffTop, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef, ws}) => {
const [entries, setEntries] = useRecoilState(entriesAtom); const [entries, setEntries] = useRecoilState(entriesAtom);
const query = useRecoilValue(queryAtom); const query = useRecoilValue(queryAtom);
const isWsConnectionClosed = ws?.current?.readyState !== WebSocket.OPEN; const isWsConnectionClosed = ws?.current?.readyState !== WebSocket.OPEN;
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>) const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const [loadMoreTop, setLoadMoreTop] = useState(false); const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false); const [isLoadingTop, setIsLoadingTop] = useState(false);
const [queriedTotal, setQueriedTotal] = useState(0);
useEffect(() => { useEffect(() => {
const list = document.getElementById('list').firstElementChild; const list = document.getElementById('list').firstElementChild;
@@ -103,6 +104,29 @@ export const EntriesList: React.FC<EntriesListProps> = ({listEntryREF, onSnapBro
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight; const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
if (ws.current) {
ws.current.addEventListener("message", (e) => {
if (!e?.data) return;
const message = JSON.parse(e.data);
switch (message.messageType) {
case "entry":
const entry = message.data;
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
const newEntries = [...entries, entry];
if (newEntries.length === 10001) {
setLeftOffTop(newEntries[0].entry.id);
newEntries.shift();
setNoMoreDataTop(false);
}
setEntries(newEntries);
break;
case "queryMetadata":
setQueriedTotal(message.data.total);
break;
};
});
}
return <React.Fragment> return <React.Fragment>
<div className={styles.list}> <div className={styles.list}>
<div id="list" ref={listEntryREF} className={styles.list}> <div id="list" ref={listEntryREF} className={styles.list}>

View File

@@ -66,8 +66,10 @@
margin-top: -60px margin-top: -60px
.capture img .capture img
height: 20px height: 14px
z-index: 1000 z-index: 1000
margin-top: 12px
margin-left: -2px
.endpointServiceContainer .endpointServiceContainer
display: flex display: flex
@@ -76,6 +78,7 @@
padding-right: 10px padding-right: 10px
padding-top: 4px padding-top: 4px
flex-grow: 1 flex-grow: 1
padding-left: 10px
.separatorRight .separatorRight
display: flex display: flex

View File

@@ -140,8 +140,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0); const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
let endpointServiceContainer = "10px";
if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
return <React.Fragment> return <React.Fragment>
<div <div
@@ -178,7 +176,7 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
{isStatusCodeEnabled && <div> {isStatusCodeEnabled && <div>
<StatusCode statusCode={entry.status} statusQuery={entry.statusQuery}/> <StatusCode statusCode={entry.status} statusQuery={entry.statusQuery}/>
</div>} </div>}
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}> <div className={styles.endpointServiceContainer}>
<Summary method={entry.method} methodQuery={entry.methodQuery} summary={entry.summary} summaryQuery={entry.summaryQuery}/> <Summary method={entry.method} methodQuery={entry.methodQuery} summary={entry.summary} summaryQuery={entry.summaryQuery}/>
<div className={styles.resolvedName}> <div className={styles.resolvedName}>
<Queryable <Queryable

View File

@@ -58,19 +58,18 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
const classes = useLayoutStyles(); const classes = useLayoutStyles();
const [entries, setEntries] = useRecoilState(entriesAtom); const setEntries = useSetRecoilState(entriesAtom);
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const query = useRecoilValue(queryAtom); const query = useRecoilValue(queryAtom);
const setTrafficViewerApiState = useSetRecoilState(trafficViewerApiAtom as RecoilState<TrafficViewerApi>) const setTrafficViewerApiState = useSetRecoilState(trafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom); const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
const [noMoreDataTop, setNoMoreDataTop] = useState(false); const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [isSnappedToBottom, setIsSnappedToBottom] = useState(true); const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
const [forceRender, setForceRender] = useState(0); const [wsReadyState, setWsReadyState] = useState(0);
const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5"); const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5");
const [queriedCurrent, setQueriedCurrent] = useState(0); const [queriedCurrent, setQueriedCurrent] = useState(0);
const [queriedTotal, setQueriedTotal] = useState(0);
const [leftOffBottom, setLeftOffBottom] = useState(0); const [leftOffBottom, setLeftOffBottom] = useState(0);
const [leftOffTop, setLeftOffTop] = useState(null); const [leftOffTop, setLeftOffTop] = useState(null);
const [truncatedTimestamp, setTruncatedTimestamp] = useState(0); const [truncatedTimestamp, setTruncatedTimestamp] = useState(0);
@@ -144,9 +143,12 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
ws.current = new WebSocket(webSocketUrl); ws.current = new WebSocket(webSocketUrl);
sendQueryWhenWsOpen(query); sendQueryWhenWsOpen(query);
ws.current.onopen = () => {
setWsReadyState(ws?.current?.readyState);
}
ws.current.onclose = () => { ws.current.onclose = () => {
if (window.location.pathname === "/") setWsReadyState(ws?.current?.readyState);
setForceRender(forceRender + 1);
} }
ws.current.onerror = (event) => { ws.current.onerror = (event) => {
console.error("WebSocket error:", event); console.error("WebSocket error:", event);
@@ -173,21 +175,10 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
} }
if (ws.current) { if (ws.current) {
ws.current.onmessage = (e) => { ws.current.addEventListener("message", (e) => {
if (!e?.data) return; if (!e?.data) return;
const message = JSON.parse(e.data); const message = JSON.parse(e.data);
switch (message.messageType) { switch (message.messageType) {
case "entry":
const entry = message.data;
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
const newEntries = [...entries, entry];
if (newEntries.length === 10001) {
setLeftOffTop(newEntries[0].entry.id);
newEntries.shift();
setNoMoreDataTop(false);
}
setEntries(newEntries);
break;
case "status": case "status":
setTappingStatus(message.tappingStatus); setTappingStatus(message.tappingStatus);
break; break;
@@ -208,7 +199,6 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
break; break;
case "queryMetadata": case "queryMetadata":
setQueriedCurrent(queriedCurrent + message.data.current); setQueriedCurrent(queriedCurrent + message.data.current);
setQueriedTotal(message.data.total);
setLeftOffBottom(message.data.leftOff); setLeftOffBottom(message.data.leftOff);
setTruncatedTimestamp(message.data.truncatedTimestamp); setTruncatedTimestamp(message.data.truncatedTimestamp);
if (leftOffTop === null) { if (leftOffTop === null) {
@@ -218,12 +208,8 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
case "startTime": case "startTime":
setStartTime(message.data); setStartTime(message.data);
break; break;
default:
console.error(
`unsupported websocket message type, Got: ${message.messageType}`
);
} }
}; })
} }
useEffect(() => { useEffect(() => {
@@ -272,7 +258,7 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
}; };
const getConnectionIndicator = () => { const getConnectionIndicator = () => {
switch (ws?.current?.readyState) { switch (wsReadyState) {
case WebSocket.OPEN: case WebSocket.OPEN:
return <div className={`${TrafficViewerStyles.indicatorContainer} ${TrafficViewerStyles.greenIndicatorContainer}`}> return <div className={`${TrafficViewerStyles.indicatorContainer} ${TrafficViewerStyles.greenIndicatorContainer}`}>
<div className={`${TrafficViewerStyles.indicator} ${TrafficViewerStyles.greenIndicator}`} /> <div className={`${TrafficViewerStyles.indicator} ${TrafficViewerStyles.greenIndicator}`} />
@@ -285,7 +271,7 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
} }
const getConnectionTitle = () => { const getConnectionTitle = () => {
switch (ws?.current?.readyState) { switch (wsReadyState) {
case WebSocket.OPEN: case WebSocket.OPEN:
return "streaming live traffic" return "streaming live traffic"
default: default:
@@ -305,9 +291,9 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
{tappingStatus && isShowStatusBar && <StatusBar isDemoBannerView={isDemoBannerView} />} {tappingStatus && isShowStatusBar && <StatusBar isDemoBannerView={isDemoBannerView} />}
<div className={TrafficViewerStyles.TrafficPageHeader}> <div className={TrafficViewerStyles.TrafficPageHeader}>
<div className={TrafficViewerStyles.TrafficPageStreamStatus}> <div className={TrafficViewerStyles.TrafficPageStreamStatus}>
<img className={TrafficViewerStyles.playPauseIcon} style={{ visibility: ws?.current?.readyState === WebSocket.OPEN ? "visible" : "hidden" }} alt="pause" <img className={TrafficViewerStyles.playPauseIcon} style={{ visibility: wsReadyState === WebSocket.OPEN ? "visible" : "hidden" }} alt="pause"
src={pauseIcon} onClick={toggleConnection} /> src={pauseIcon} onClick={toggleConnection} />
<img className={TrafficViewerStyles.playPauseIcon} style={{ position: "absolute", visibility: ws?.current?.readyState === WebSocket.OPEN ? "hidden" : "visible" }} alt="play" <img className={TrafficViewerStyles.playPauseIcon} style={{ position: "absolute", visibility: wsReadyState === WebSocket.OPEN ? "hidden" : "visible" }} alt="play"
src={playIcon} onClick={toggleConnection} /> src={playIcon} onClick={toggleConnection} />
<div className={TrafficViewerStyles.connectionText}> <div className={TrafficViewerStyles.connectionText}>
{getConnectionTitle()} {getConnectionTitle()}
@@ -331,8 +317,6 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
setIsSnappedToBottom={setIsSnappedToBottom} setIsSnappedToBottom={setIsSnappedToBottom}
queriedCurrent={queriedCurrent} queriedCurrent={queriedCurrent}
setQueriedCurrent={setQueriedCurrent} setQueriedCurrent={setQueriedCurrent}
queriedTotal={queriedTotal}
setQueriedTotal={setQueriedTotal}
startTime={startTime} startTime={startTime}
noMoreDataTop={noMoreDataTop} noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop} setNoMoreDataTop={setNoMoreDataTop}

View File

@@ -1,18 +1,17 @@
import React, { CSSProperties } from "react"; import React, { CSSProperties } from "react";
import infoImg from 'assets/info.svg';
import styles from "./style/InformationIcon.module.sass" import styles from "./style/InformationIcon.module.sass"
const DEFUALT_LINK = "https://getmizu.io/docs" const DEFUALT_LINK = "https://getmizu.io/docs"
export interface InformationIconProps{ export interface InformationIconProps {
link?: string, link?: string,
style? : CSSProperties style?: CSSProperties
} }
export const InformationIcon: React.FC<InformationIconProps> = ({link,style}) => { export const InformationIcon: React.FC<InformationIconProps> = ({ link, style }) => {
return <React.Fragment> return <React.Fragment>
<a href={DEFUALT_LINK ? DEFUALT_LINK : link} style={style} className={styles.flex} title="documentation" target="_blank"> <a href={DEFUALT_LINK ? DEFUALT_LINK : link} style={style} className={styles.linkStyle} title="documentation" target="_blank">
<img className="headerIcon" src={infoImg} alt="Info icon"/> <span>Docs</span>
</a> </a>
</React.Fragment> </React.Fragment>
} }

View File

@@ -54,7 +54,7 @@ const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal}) => {
backgroundColor: protocol.backgroundColor, backgroundColor: protocol.backgroundColor,
color: protocol.foregroundColor, color: protocol.foregroundColor,
fontSize: protocol.fontSize, fontSize: protocol.fontSize,
marginRight: "-20px", marginRight: "-6px",
}} }}
title={protocol.longName} title={protocol.longName}
> >

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 21H6.14286C5.07143 21 4 20.32 4 18.96C4 17.6 5.07143 16.92 6.14286 16.92H19V4H6.14286C5.07143 4 4 5.02 4 6.04V18.96M16.8571 17.6V20.32V17.6Z" stroke="#627EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="8" y="7" width="7" height="2" fill="#627EF7"/>
<rect x="8" y="11" width="4" height="2" fill="#627EF7"/>
</svg>

Before

Width:  |  Height:  |  Size: 454 B

View File

@@ -1,2 +1,8 @@
.flex .linkStyle
display: flex display: flex
color: #18253d
text-decoration: none
font-family: "Ubuntu", sans-serif
font-style: normal
font-weight: 600
font-size: 14px

View File

@@ -4,7 +4,7 @@
position: absolute position: absolute
transform: translate(-50%, -3px) transform: translate(-50%, -3px)
left: 50% left: 50%
z-index: 9999 z-index: 100
min-width: 200px min-width: 200px
background: $blue-color background: $blue-color
color: rgba(255,255,255,0.75) color: rgba(255,255,255,0.75)
@@ -19,7 +19,7 @@
overflow: hidden overflow: hidden
max-width: clamp(150px,50%,600px) max-width: clamp(150px,50%,600px)
&.banner &.banner
top: 53px top: 53px
.podsCount .podsCount
@@ -41,7 +41,7 @@
table table
width: 100% width: 100%
margin-top: 20px margin-top: 20px
tbody tbody
max-height: 70vh max-height: 70vh
overflow-y: auto overflow-y: auto

View File

@@ -9,7 +9,7 @@
text-align: center text-align: center
line-height: 22px line-height: 22px
font-weight: 600 font-weight: 600
margin-left: 8px margin-left: 3px
.neutral .neutral
background: gray background: gray

View File

@@ -48,7 +48,8 @@
"npm-link-shared": "^0.5.6", "npm-link-shared": "^0.5.6",
"react-app-rewire-alias": "^1.1.7", "react-app-rewire-alias": "^1.1.7",
"react-dev-utils": "^12.0.0", "react-dev-utils": "^12.0.0",
"recoil": "^0.5.2" "recoil": "^0.5.2",
"react-error-overlay": "6.0.9"
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",

View File

@@ -1,9 +1,9 @@
import React, {useEffect, useState} from "react"; import React, { useState } from "react";
import { Button } from "@material-ui/core"; import { Button } from "@material-ui/core";
import Api, { MizuWebsocketURL } from "../../../helpers/api"; import Api, { MizuWebsocketURL } from "../../../helpers/api";
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import {useSetRecoilState, useRecoilState} from "recoil"; import { useRecoilState } from "recoil";
import {useCommonStyles} from "../../../helpers/commonStyle" import { useCommonStyles } from "../../../helpers/commonStyle"
import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen"; import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen";
import TrafficViewer from "@up9/mizu-common" import TrafficViewer from "@up9/mizu-common"
import "@up9/mizu-common/dist/index.css" import "@up9/mizu-common/dist/index.css"
@@ -17,13 +17,13 @@ interface TrafficPageProps {
const api = Api.getInstance(); const api = Api.getInstance();
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus}) => { export const TrafficPage: React.FC<TrafficPageProps> = ({ setAnalyzeStatus }) => {
const commonClasses = useCommonStyles(); const commonClasses = useCommonStyles();
const setServiceMapModalOpen = useSetRecoilState(serviceMapModalOpenAtom); const [serviceMapModalOpen, setServiceMapModalOpen] = useRecoilState(serviceMapModalOpenAtom);
const [openOasModal, setOpenOasModal] = useRecoilState(oasModalOpenAtom); const [openOasModal, setOpenOasModal] = useRecoilState(oasModalOpenAtom);
const [openWebSocket, setOpenWebSocket] = useState(true); const [openWebSocket, setOpenWebSocket] = useState(true);
const trafficViewerApi = {...api} const trafficViewerApi = { ...api }
const handleOpenOasModal = () => { const handleOpenOasModal = () => {
setOpenWebSocket(false) setOpenWebSocket(false)
@@ -36,37 +36,31 @@ const trafficViewerApi = {...api}
}, 500); }, 500);
const actionButtons = (window["isOasEnabled"] || window["isServiceMapEnabled"]) && const actionButtons = (window["isOasEnabled"] || window["isServiceMapEnabled"]) &&
<div style={{ display: 'flex', height: "100%" }}> <div style={{ display: 'flex', height: "100%" }}>
{window["isOasEnabled"] && <Button {window["isOasEnabled"] && <Button
startIcon={<img className="custom" src={services} alt="services"></img>} startIcon={<img className="custom" src={services} alt="services"></img>}
size="large" size="large"
variant="contained" variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton} className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
style={{ marginRight: 25, textTransform: 'unset' }} style={{ marginRight: 25, textTransform: 'unset' }}
onClick={handleOpenOasModal}> onClick={handleOpenOasModal}>
OpenAPI Specs Service Catalog
</Button>} </Button>}
{window["isServiceMapEnabled"] && <Button {window["isServiceMapEnabled"] && <Button
startIcon={<img src={serviceMap} className="custom" alt="service-map" style={{marginRight:"8%"}}></img>} startIcon={<img src={serviceMap} className="custom" alt="service-map" style={{ marginRight: "8%" }}></img>}
size="large" size="large"
variant="contained" variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton} className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={openServiceMapModalDebounce} onClick={openServiceMapModalDebounce}
style={{textTransform: 'unset'}}> style={{ textTransform: 'unset' }}>
Service Map Service Map
</Button>} </Button>}
</div> </div>
useEffect(() => {
return () => {
//closeSocket()
}
},[])
return ( return (
<> <>
<TrafficViewer setAnalyzeStatus={setAnalyzeStatus} webSocketUrl={MizuWebsocketURL} isCloseWebSocket={!openWebSocket} <TrafficViewer setAnalyzeStatus={setAnalyzeStatus} webSocketUrl={MizuWebsocketURL} isCloseWebSocket={!openWebSocket}
trafficViewerApiProp={trafficViewerApi} actionButtons={actionButtons} isShowStatusBar={!openOasModal} isDemoBannerView={false}/> trafficViewerApiProp={trafficViewerApi} actionButtons={actionButtons} isShowStatusBar={!(openOasModal || serviceMapModalOpen)} isDemoBannerView={false} />
</> </>
); );
}; };

View File

@@ -100,6 +100,7 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen
const commonClasses = useCommonStyles(); const commonClasses = useCommonStyles();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] }); const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
const [graphOptions, setGraphOptions] = useState(ServiceMapOptions);
const getServiceMapData = useCallback(async () => { const getServiceMapData = useCallback(async () => {
try { try {
@@ -149,6 +150,14 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen
// eslint-disable-next-line // eslint-disable-next-line
}, [isOpen]) }, [isOpen])
useEffect(() => {
if(graphData?.nodes?.length === 0) return;
let options = {...graphOptions};
options.physics.barnesHut.avoidOverlap = graphData?.nodes?.length > 10 ? 0 : 1;
setGraphOptions(options);
// eslint-disable-next-line
},[graphData?.nodes?.length])
useEffect(() => { useEffect(() => {
getServiceMapData(); getServiceMapData();
return () => setGraphData({ nodes: [], edges: [] }) return () => setGraphData({ nodes: [], edges: [] })
@@ -180,7 +189,7 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen
<div style={{ display: "flex", justifyContent: "space-between" }}> <div style={{ display: "flex", justifyContent: "space-between" }}>
<div> <div>
<Button <Button
startIcon={<img src={refresh} className="custom" alt="refresh" style={{ marginRight: "8%" }}></img>} startIcon={<img src={refresh} className="custom" alt="refresh" style={{ marginRight: "8%" }}/>}
size="medium" size="medium"
variant="contained" variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton} className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
@@ -189,20 +198,20 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen
Refresh Refresh
</Button> </Button>
</div> </div>
<img src={close} alt="close" onClick={() => onClose()} style={{ cursor: "pointer" }}></img> <img src={close} alt="close" onClick={() => onClose()} style={{ cursor: "pointer" }}/>
</div> </div>
<Graph <Graph
graph={graphData} graph={graphData}
options={ServiceMapOptions} options={graphOptions}
/> />
<div className='legend-scale'> <div className='legend-scale'>
<ul className='legend-labels'> <ul className='legend-labels'>
<li><span style={{ background: '#205cf5' }}></span>HTTP</li> <li><span style={{ background: '#205cf5' }}/>HTTP</li>
<li><span style={{ background: '#244c5a' }}></span>HTTP/2</li> <li><span style={{ background: '#244c5a' }}/>HTTP/2</li>
<li><span style={{ background: '#244c5a' }}></span>gRPC</li> <li><span style={{ background: '#244c5a' }}/>gRPC</li>
<li><span style={{ background: '#ff6600' }}></span>AMQP</li> <li><span style={{ background: '#ff6600' }}/>AMQP</li>
<li><span style={{ background: '#000000' }}></span>KAFKA</li> <li><span style={{ background: '#000000' }}/>KAFKA</li>
<li><span style={{ background: '#a41e11' }}></span>REDIS</li> <li><span style={{ background: '#a41e11' }}/>REDIS</li>
</ul> </ul>
</div> </div>
</div>} </div>}
@@ -211,4 +220,4 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen
</Modal> </Modal>
); );
} }

View File

@@ -148,7 +148,7 @@ const ServiceMapOptions = {
springLength: 180, springLength: 180,
springConstant: 0.04, springConstant: 0.04,
damping: 0.2, damping: 0.2,
avoidOverlap: 1 avoidOverlap: 0
}, },
}, },
layout: { layout: {
@@ -171,4 +171,4 @@ const ServiceMapOptions = {
}, },
}; };
export default ServiceMapOptions export default ServiceMapOptions