Skip to content

Commit 97d9409

Browse files
Add Redis Graph module (GRAPH.QUERY, GRAPH.SLOWLOG) (#157)
* Update docker to use 7.4.0 * Add Graph.Query * Add Redis Graph * Add Test data for Redis Graph * Add RedisGraph to 7.4 dashboard * Implementing graph * Implement slowlog. * Change default query on dashboard * Revert docker configuration * Update dashboard * Format Co-authored-by: Andrei Shamanau <[email protected]>
1 parent 3e20888 commit 97d9409

File tree

10 files changed

+752
-1
lines changed

10 files changed

+752
-1
lines changed

data/dump.rdb

306 KB
Binary file not shown.

pkg/query.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ func query(ctx context.Context, query backend.DataQuery, client redisClient) bac
127127
case "rg.pyexecute":
128128
return queryRgPyexecute(qm, client)
129129

130+
/**
131+
* Redis Graph
132+
*/
133+
case "graph.query":
134+
return queryGraphQuery(qm, client)
135+
case "graph.slowlog":
136+
return queryGraphSlowlog(qm, client)
137+
130138
/**
131139
* Default
132140
*/

pkg/query_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ func TestQuery(t *testing.T) {
4646
{queryModel{Command: "rg.pyexecute"}},
4747
{queryModel{Command: "rg.xrange"}},
4848
{queryModel{Command: "rg.xrevrange"}},
49+
{queryModel{Command: "graph.query"}},
50+
{queryModel{Command: "graph.slowlog"}},
4951
}
5052

5153
// Run Tests

pkg/redis-graph.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package main
2+
3+
import (
4+
"strconv"
5+
"time"
6+
7+
"github.com/grafana/grafana-plugin-sdk-go/backend"
8+
"github.com/grafana/grafana-plugin-sdk-go/data"
9+
)
10+
11+
/**
12+
* Represents node
13+
*/
14+
type nodeEntry struct {
15+
id string
16+
title string
17+
subTitle string
18+
mainStat string
19+
arc int64
20+
}
21+
22+
/**
23+
* Represents edge
24+
*/
25+
type edgeEntry struct {
26+
id string
27+
source string
28+
target string
29+
mainStat string
30+
}
31+
32+
/**
33+
* GRAPH.QUERY <Graph name> {query}
34+
*
35+
* Executes the given query against a specified graph.
36+
* @see https://oss.redislabs.com/redisgraph/commands/#graphquery
37+
*/
38+
func queryGraphQuery(qm queryModel, client redisClient) backend.DataResponse {
39+
response := backend.DataResponse{}
40+
41+
var result []interface{}
42+
43+
// Run command
44+
err := client.RunFlatCmd(&result, "GRAPH.QUERY", qm.Key, qm.Cypher)
45+
46+
// Check error
47+
if err != nil {
48+
return errorHandler(response, err)
49+
}
50+
51+
// New Frame for nodes
52+
frameWithNodes := data.NewFrame("nodes")
53+
frameWithNodes.Meta = &data.FrameMeta{
54+
PreferredVisualization: "nodeGraph",
55+
}
56+
frameWithNodes.Fields = append(frameWithNodes.Fields, data.NewField("id", nil, []string{}))
57+
frameWithNodes.Fields = append(frameWithNodes.Fields, data.NewField("title", nil, []string{}))
58+
frameWithNodes.Fields = append(frameWithNodes.Fields, data.NewField("subTitle", nil, []string{}))
59+
frameWithNodes.Fields = append(frameWithNodes.Fields, data.NewField("mainStat", nil, []string{}))
60+
frameWithNodes.Fields = append(frameWithNodes.Fields, data.NewField("arc__", nil, []int64{}))
61+
62+
// New Frame for edges
63+
frameWithEdges := data.NewFrame("edges")
64+
frameWithEdges.Meta = &data.FrameMeta{
65+
PreferredVisualization: "nodeGraph",
66+
}
67+
frameWithEdges.Fields = append(frameWithEdges.Fields, data.NewField("id", nil, []string{}))
68+
frameWithEdges.Fields = append(frameWithEdges.Fields, data.NewField("source", nil, []string{}))
69+
frameWithEdges.Fields = append(frameWithEdges.Fields, data.NewField("target", nil, []string{}))
70+
frameWithEdges.Fields = append(frameWithEdges.Fields, data.NewField("mainStat", nil, []string{}))
71+
72+
// Adding frames to response
73+
response.Frames = append(response.Frames, frameWithNodes)
74+
response.Frames = append(response.Frames, frameWithEdges)
75+
76+
existingNodes := map[string]bool{}
77+
78+
for _, entries := range result[1].([]interface{}) {
79+
nodes, edges := findAllNodesAndEdges(entries)
80+
for _, node := range nodes {
81+
// Add each nodeEntry only once
82+
if _, ok := existingNodes[node.id]; !ok {
83+
frameWithNodes.AppendRow(node.id, node.title, node.subTitle, node.mainStat, node.arc)
84+
existingNodes[node.id] = true
85+
}
86+
}
87+
for _, edge := range edges {
88+
frameWithEdges.AppendRow(edge.id, edge.source, edge.target, edge.mainStat)
89+
}
90+
}
91+
return response
92+
}
93+
94+
/**
95+
* Parse array of entries and find
96+
* either Nodes https://oss.redislabs.com/redisgraph/result_structure/#nodes
97+
* or Relations https://oss.redislabs.com/redisgraph/result_structure/#relations
98+
* and create corresponding nodeEntry or edgeEntry
99+
**/
100+
func findAllNodesAndEdges(input interface{}) ([]nodeEntry, []edgeEntry) {
101+
nodes := []nodeEntry{}
102+
edges := []edgeEntry{}
103+
104+
if entries, ok := input.([]interface{}); ok {
105+
for _, entry := range entries {
106+
entryFields := entry.([]interface{})
107+
108+
// Node https://oss.redislabs.com/redisgraph/result_structure/#nodes
109+
if len(entryFields) == 3 {
110+
node := nodeEntry{arc: 1}
111+
idArray := entryFields[0].([]interface{})
112+
node.id = strconv.FormatInt(idArray[1].(int64), 10)
113+
114+
// Assume first label will be a title if exists
115+
labelsArray := entryFields[1].([]interface{})
116+
labels := labelsArray[1].([]interface{})
117+
if len(labels) > 0 {
118+
node.title = string(labels[0].([]byte))
119+
}
120+
121+
// Assume first property will be a mainStat if exists
122+
propertiesArray := entryFields[2].([]interface{})
123+
properties := propertiesArray[1].([]interface{})
124+
if len(properties) > 0 {
125+
propertyArray := properties[0].([]interface{})
126+
switch propValue := propertyArray[1].(type) {
127+
case []byte:
128+
node.mainStat = string(propValue)
129+
case int64:
130+
node.mainStat = strconv.FormatInt(propValue, 10)
131+
}
132+
}
133+
134+
nodes = append(nodes, node)
135+
}
136+
137+
// Relation https://oss.redislabs.com/redisgraph/result_structure/#relations
138+
if len(entryFields) == 5 {
139+
edge := edgeEntry{}
140+
idArray := entryFields[0].([]interface{})
141+
edge.id = strconv.FormatInt(idArray[1].(int64), 10)
142+
143+
// Main Stat
144+
typeArray := entryFields[1].([]interface{})
145+
edge.mainStat = string(typeArray[1].([]byte))
146+
147+
// Source
148+
srcArray := entryFields[2].([]interface{})
149+
edge.source = strconv.FormatInt(srcArray[1].(int64), 10)
150+
151+
// Target
152+
destArray := entryFields[3].([]interface{})
153+
edge.target = strconv.FormatInt(destArray[1].(int64), 10)
154+
155+
edges = append(edges, edge)
156+
}
157+
}
158+
}
159+
return nodes, edges
160+
}
161+
162+
/**
163+
* GRAPH.SLOWLOG <Graph name>
164+
*
165+
* Returns a list containing up to 10 of the slowest queries issued against the given graph ID.
166+
* @see https://oss.redislabs.com/redisgraph/commands/#graphslowlog
167+
*/
168+
func queryGraphSlowlog(qm queryModel, client redisClient) backend.DataResponse {
169+
response := backend.DataResponse{}
170+
171+
var result [][]string
172+
173+
// Run command
174+
err := client.RunFlatCmd(&result, "GRAPH.SLOWLOG", qm.Key)
175+
176+
// Check error
177+
if err != nil {
178+
return errorHandler(response, err)
179+
}
180+
181+
// New Frame
182+
frame := data.NewFrame("GRAPH.SLOWLOG")
183+
frame.Fields = append(frame.Fields, data.NewField("timestamp", nil, []time.Time{}))
184+
frame.Fields = append(frame.Fields, data.NewField("command", nil, []string{}))
185+
frame.Fields = append(frame.Fields, data.NewField("query", nil, []string{}))
186+
frame.Fields = append(frame.Fields, data.NewField("duration", nil, []float64{}))
187+
response.Frames = append(response.Frames, frame)
188+
189+
// Set Field Config
190+
frame.Fields[3].Config = &data.FieldConfig{Unit: "µs"}
191+
192+
// Entries
193+
for _, entry := range result {
194+
timestamp, _ := strconv.ParseInt(entry[0], 10, 64)
195+
duration, _ := strconv.ParseFloat(entry[3], 64)
196+
frame.AppendRow(time.Unix(timestamp, 0), entry[1], entry[2], duration)
197+
}
198+
199+
// Return
200+
return response
201+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// +build integration
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
9+
"github.com/mediocregopher/radix/v3"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
/**
14+
* GRAPH.QUERY
15+
*/
16+
func TestGraphQueryIntegration(t *testing.T) {
17+
// Client
18+
radixClient, _ := radix.NewPool("tcp", fmt.Sprintf("127.0.0.1:%d", integrationTestPort), 10)
19+
client := radixV3Impl{radixClient: radixClient}
20+
21+
// Response
22+
resp := queryGraphQuery(queryModel{Command: "graph.query", Key: "GOT_DEMO", Cypher: "MATCH (w:writer)-[r:wrote]->(b:book) return w,r,b"}, &client)
23+
require.Len(t, resp.Frames, 2)
24+
require.Len(t, resp.Frames[0].Fields, 5)
25+
require.Equal(t, "id", resp.Frames[0].Fields[0].Name)
26+
require.Equal(t, "title", resp.Frames[0].Fields[1].Name)
27+
require.Equal(t, "subTitle", resp.Frames[0].Fields[2].Name)
28+
require.Equal(t, "mainStat", resp.Frames[0].Fields[3].Name)
29+
require.Equal(t, "arc__", resp.Frames[0].Fields[4].Name)
30+
require.Equal(t, 15, resp.Frames[0].Fields[0].Len())
31+
require.Len(t, resp.Frames[1].Fields, 4)
32+
require.Equal(t, "id", resp.Frames[1].Fields[0].Name)
33+
require.Equal(t, "source", resp.Frames[1].Fields[1].Name)
34+
require.Equal(t, "target", resp.Frames[1].Fields[2].Name)
35+
require.Equal(t, "mainStat", resp.Frames[1].Fields[3].Name)
36+
require.Equal(t, 14, resp.Frames[1].Fields[0].Len())
37+
}
38+
39+
func TestGraphQueryIntegrationWithoutRelations(t *testing.T) {
40+
// Client
41+
radixClient, _ := radix.NewPool("tcp", fmt.Sprintf("127.0.0.1:%d", integrationTestPort), 10)
42+
client := radixV3Impl{radixClient: radixClient}
43+
44+
// Response
45+
resp := queryGraphQuery(queryModel{Command: "graph.query", Key: "GOT_DEMO", Cypher: "MATCH (w:writer)-[wrote]->(b:book) return w,b"}, &client)
46+
require.Len(t, resp.Frames, 2)
47+
require.Len(t, resp.Frames[0].Fields, 5)
48+
require.Equal(t, 15, resp.Frames[0].Fields[0].Len())
49+
require.Len(t, resp.Frames[1].Fields, 4)
50+
require.Equal(t, 0, resp.Frames[1].Fields[0].Len())
51+
}
52+
53+
func TestGraphQueryIntegrationWithoutNodes(t *testing.T) {
54+
// Client
55+
radixClient, _ := radix.NewPool("tcp", fmt.Sprintf("127.0.0.1:%d", integrationTestPort), 10)
56+
client := radixV3Impl{radixClient: radixClient}
57+
58+
// Response
59+
resp := queryGraphQuery(queryModel{Command: "graph.query", Key: "GOT_DEMO", Cypher: "MATCH (w:writer)-[r:wrote]->(b:book) return r"}, &client)
60+
require.Len(t, resp.Frames, 2)
61+
require.Len(t, resp.Frames[0].Fields, 5)
62+
require.Equal(t, 0, resp.Frames[0].Fields[0].Len())
63+
require.Len(t, resp.Frames[1].Fields, 4)
64+
require.Equal(t, 14, resp.Frames[1].Fields[0].Len())
65+
}
66+
67+
/**
68+
* GRAPH.SLOWLOG
69+
*/
70+
func TestGraphSlowlogIntegration(t *testing.T) {
71+
// Client
72+
radixClient, _ := radix.NewPool("tcp", fmt.Sprintf("127.0.0.1:%d", integrationTestPort), 10)
73+
client := radixV3Impl{radixClient: radixClient}
74+
75+
// Response
76+
resp := queryGraphSlowlog(queryModel{Command: "graph.slowlog", Key: "GOT_DEMO"}, &client)
77+
require.Len(t, resp.Frames, 1)
78+
require.Len(t, resp.Frames[0].Fields, 4)
79+
}

0 commit comments

Comments
 (0)