Skip to content

Commit 8029c8e

Browse files
authored
Merge pull request #79 from valkey-io/integrate-metrics
Integrate metrics into Electron app
2 parents 2bca988 + 3709db9 commit 8029c8e

34 files changed

+4129
-1250
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ out/
3939
app/
4040
packages/
4141

42-
tools/valkey-metrics/data/
42+
apps/metrics/data/

apps/frontend/electron.main.js

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
const { app, BrowserWindow, Menu } = require('electron');
1+
const { app, BrowserWindow } = require('electron');
22
const path = require('path');
33
const { fork } = require('child_process');
44

55
let serverProcess;
6+
let metricsProcesses = new Map();
67

78
function startServer() {
89
if (app.isPackaged) {
@@ -19,6 +20,61 @@ function startServer() {
1920
}
2021
}
2122

23+
function startMetrics(serverConnectionId, serverConnectionDetails) {
24+
const dataDir = path.join(app.getPath('userData'), 'metrics-data', serverConnectionId);
25+
26+
let metricsServerPath;
27+
let configPath;
28+
29+
if (app.isPackaged) {
30+
metricsServerPath = path.join(process.resourcesPath, 'server-metrics.js');
31+
configPath = path.join(process.resourcesPath, 'config.yml'); // Path for production
32+
} else {
33+
metricsServerPath = path.join(__dirname, '../../metrics/src/index.js');
34+
configPath = path.join(__dirname, '../../metrics/config.yml'); // Path for development
35+
}
36+
37+
console.log(`Starting metrics server for connection ${serverConnectionId}...`);
38+
39+
const metricsProcess = fork(metricsServerPath, [], {
40+
env: {
41+
...process.env,
42+
PORT: 0,
43+
DATA_DIR: dataDir,
44+
VALKEY_URL: `valkey://${serverConnectionDetails.host}:${serverConnectionDetails.port}`,
45+
CONFIG_PATH: configPath // Explicitly provide the config path
46+
}
47+
});
48+
49+
metricsProcesses.set(serverConnectionId, metricsProcess);
50+
51+
metricsProcess.on('message', (message) => {
52+
if (message && message.type === 'metrics-started') {
53+
console.log(`Metrics server for ${serverConnectionId} started successfully on host: ${message.payload.metricsHost} port ${message.payload.metricsPort}`);
54+
}
55+
});
56+
57+
metricsProcess.on('close', (code) => {
58+
console.log(`Metrics server for connection ${serverConnectionId} exited with code ${code}`);
59+
metricsProcesses.delete(serverConnectionId);
60+
});
61+
62+
metricsProcess.on('error', (err) => {
63+
console.error(`Metrics server for connection ${serverConnectionId} error: ${err}`);
64+
});
65+
}
66+
67+
// Disconnect functionality in the server has not been implemented. Once that is implemented, this can be used.
68+
function stopMetricServer(serverConnectionId) {
69+
metricsProcesses.get(serverConnectionId).kill();
70+
}
71+
72+
function stopMetricServers() {
73+
metricsProcesses.forEach((_serverConnectionId, metricProcess) => {
74+
metricProcess.kill();
75+
})
76+
}
77+
2278
function createWindow() {
2379
const win = new BrowserWindow({
2480
width: 1200,
@@ -31,7 +87,6 @@ function createWindow() {
3187

3288
if (app.isPackaged) {
3389
win.loadFile(path.join(__dirname, 'dist', 'index.html'));
34-
//win.webContents.openDevTools(); // open DevTools for debugging
3590
} else {
3691
win.loadURL('http://localhost:5173');
3792
win.webContents.openDevTools();
@@ -42,8 +97,20 @@ app.whenReady().then(() => {
4297
startServer();
4398
if (serverProcess) {
4499
serverProcess.on('message', (message) => {
45-
if (message === 'ready') {
46-
createWindow();
100+
switch (message.type) {
101+
case 'websocket-ready':
102+
createWindow();
103+
break;
104+
case 'valkeyConnection/standaloneConnectFulfilled':
105+
startMetrics(message.payload.connectionId, message.payload.connectionDetails);
106+
break;
107+
default:
108+
try {
109+
console.log(`Received unknown server message: ${JSON.stringify(message)}`);
110+
} catch (_) {
111+
console.log(`Received unknown server message: ${message}`);
112+
}
113+
47114
}
48115
});
49116
} else {
@@ -61,6 +128,9 @@ app.on('before-quit', () => {
61128
if (serverProcess) {
62129
serverProcess.kill();
63130
}
131+
if (metricsProcesses.length > 0) {
132+
stopMetricServers();
133+
}
64134
});
65135

66136
app.on('activate', () => {

apps/frontend/src/state/epics/rootEpic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Store } from "@reduxjs/toolkit"
77
export const registerEpics = (store: Store) => {
88
merge(
99
wsConnectionEpic(store),
10-
connectionEpic(store),
10+
connectionEpic(),
1111
autoReconnectEpic(store),
1212
valkeyRetryEpic(store),
1313
deleteConnectionEpic(),

apps/frontend/src/state/epics/valkeyEpics.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { merge, timer, EMPTY } from "rxjs"
2-
import { ignoreElements, tap, delay, switchMap, catchError } from "rxjs/operators"
2+
import { ignoreElements, tap, delay, switchMap, catchError, filter } from "rxjs/operators"
33
import * as R from "ramda"
44
import { DISCONNECTED, LOCAL_STORAGE, NOT_CONNECTED, RETRY_CONFIG, retryDelay } from "@common/src/constants.ts"
55
import { toast } from "sonner"
66
import { getSocket } from "./wsEpics"
7-
import { connectFulfilled, connectPending, deleteConnection, connectRejected, startRetry, stopRetry }
8-
from "../valkey-features/connection/connectionSlice"
7+
import {
8+
standaloneConnectFulfilled,
9+
clusterConnectFulfilled,
10+
connectPending,
11+
deleteConnection,
12+
connectRejected,
13+
startRetry,
14+
stopRetry
15+
} from "../valkey-features/connection/connectionSlice"
916
import { sendRequested } from "../valkey-features/command/commandSlice"
1017
import { setData } from "../valkey-features/info/infoSlice"
1118
import { action$, select } from "../middleware/rxjsMiddleware/rxjsMiddlware"
1219
import { setClusterData } from "../valkey-features/cluster/clusterSlice"
1320
import { connectFulfilled as wsConnectFulfilled } from "../wsconnection/wsConnectionSlice"
1421
import history from "../../history.ts"
1522
import type { Store } from "@reduxjs/toolkit"
16-
import { atId } from "@/state/valkey-features/connection/connectionSelectors.ts"
1723

18-
export const connectionEpic = (store: Store) =>
24+
export const connectionEpic = () =>
1925
merge(
2026
action$.pipe(
2127
select(connectPending),
@@ -28,21 +34,24 @@ export const connectionEpic = (store: Store) =>
2834
),
2935

3036
action$.pipe(
31-
select(connectFulfilled),
32-
tap(({ payload: { connectionId } }) => {
37+
select(standaloneConnectFulfilled),
38+
tap(({ payload }) => {
3339
try {
3440
const currentConnections = R.pipe(
3541
(v: string) => localStorage.getItem(v),
3642
(s) => (s === null ? {} : JSON.parse(s)),
3743
)(LOCAL_STORAGE.VALKEY_CONNECTIONS)
3844

39-
R.pipe( // merge fulfilled connection with existing connections in localStorage
40-
atId(connectionId),
41-
R.evolve({ status: R.always(NOT_CONNECTED) }),
42-
R.assoc(connectionId, R.__, currentConnections),
45+
R.pipe(
46+
(p) => ({
47+
connectionDetails: p.connectionDetails,
48+
status: NOT_CONNECTED,
49+
}),
50+
(connectionToSave) => ({ ...currentConnections, [payload.connectionId]: connectionToSave }),
4351
JSON.stringify,
4452
(updated) => localStorage.setItem(LOCAL_STORAGE.VALKEY_CONNECTIONS, updated),
45-
)(store.getState())
53+
)(payload)
54+
4655
toast.success("Connected to server successfully!")
4756
} catch (e) {
4857
toast.error("Connection to server failed!")
@@ -166,9 +175,8 @@ export const deleteConnectionEpic = () =>
166175
R.dissoc(connectionId),
167176
JSON.stringify,
168177
(updated) => localStorage.setItem(LOCAL_STORAGE.VALKEY_CONNECTIONS, updated),
178+
() => toast.success("Connection removed successfully!"),
169179
)(currentConnections)
170-
171-
toast.success("Connection removed successfully!")
172180
} catch (e) {
173181
toast.error("Failed to remove connection!")
174182
console.error(e)
@@ -187,13 +195,20 @@ export const sendRequestEpic = () =>
187195

188196
export const setDataEpic = () =>
189197
action$.pipe(
190-
select(connectFulfilled),
198+
filter(
199+
({ type }) =>
200+
type === standaloneConnectFulfilled.type ||
201+
type === clusterConnectFulfilled.type,
202+
),
191203
tap((action) => {
192204
const socket = getSocket()
205+
193206
const { clusterId, connectionId } = action.payload
194-
if (clusterId) {
207+
208+
if (action.type === clusterConnectFulfilled.type) {
195209
socket.next({ type: setClusterData.type, payload: { clusterId, connectionId } })
196210
}
211+
197212
socket.next({ type: setData.type, payload: { connectionId } })
198213
const dashboardPath = clusterId
199214
? `/${clusterId}/${connectionId}/dashboard`

apps/frontend/src/state/valkey-features/connection/connectionSlice.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,28 @@ const connectionSlice = createSlice({
6868
}),
6969
}
7070
},
71-
connectFulfilled: (state, action) => {
71+
standaloneConnectFulfilled: (
72+
state,
73+
action: PayloadAction<{
74+
connectionId: string;
75+
connectionDetails: { host: string; port: number};
76+
}>,
77+
) => {
78+
const { connectionId } = action.payload
79+
const connectionState = state.connections[connectionId]
80+
if (connectionState) {
81+
connectionState.status = CONNECTED
82+
connectionState.errorMessage = null
83+
}
84+
},
85+
clusterConnectFulfilled: (
86+
state,
87+
action: PayloadAction<{
88+
connectionId: string;
89+
clusterNodes: Record<string, ConnectionDetails>;
90+
clusterId: string;
91+
}>,
92+
) => {
7293
const { connectionId, clusterNodes, clusterId } = action.payload
7394
const connectionState = state.connections[connectionId]
7495
if (connectionState) {
@@ -140,10 +161,11 @@ const connectionSlice = createSlice({
140161
})
141162

142163
export default connectionSlice.reducer
143-
export const {
144-
connectPending,
145-
connectFulfilled,
146-
connectRejected,
164+
export const {
165+
connectPending,
166+
standaloneConnectFulfilled,
167+
clusterConnectFulfilled,
168+
connectRejected,
147169
connectionBroken,
148170
closeConnection,
149171
updateConnectionDetails,

apps/metrics/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Timestream Metrics
2+
3+
## Overview
4+
Standalone service to poll info, memory and other data from Valkey cluster and store in files to produce timeseries of CPU load changes and other metrics trends over time.
5+
6+
## Getting started
7+
**Note:** valkey-cluster must be running before the following steps.
8+
9+
`docker compose up --build`
10+
11+
To examine the metrics with curl:
12+
`curl -s http://localhost:3000/slowlog | jq`
13+
`curl -s http://localhost:3000/cpu | jq`
14+
`curl -s http://localhost:3000/memory | jq`
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ valkey:
33
# url: "valkey://:password@my-cluster.example.com:6379"
44

55
server:
6-
port: 3000
6+
port: 0
77
data_dir: "/app/data"
88

99
collector:

0 commit comments

Comments
 (0)