Skip to content

Commit 8bf679b

Browse files
authored
Merge pull request #46 from Omniaevo/develop
Log monitored topics to file beside system notifications
2 parents d7ecf40 + ee8659b commit 8bf679b

File tree

4 files changed

+161
-19
lines changed

4 files changed

+161
-19
lines changed

package-lock.json

+11-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mqtt5-explorer",
3-
"version": "1.13.1",
3+
"version": "1.14.0",
44
"private": false,
55
"license": "GPLv3",
66
"description": "A simple MQTT client that supports MQTT5 protocol.",
@@ -37,6 +37,7 @@
3737
"core-js": "^3.27.0",
3838
"electron-store": "^8.1.0",
3939
"electron-updater": "^6.1.7",
40+
"moment": "^2.30.1",
4041
"mqtt": "^4.3.7",
4142
"register-service-worker": "^1.7.2",
4243
"uuid": "^8.3.2",

src/utils/MessageLogger.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os from "os";
2+
import path from "path";
3+
import fs from "fs";
4+
import * as moment from "moment";
5+
6+
class MessageLogger {
7+
#subPath = "";
8+
#basePath = "";
9+
#separator = path.sep;
10+
11+
#writing = false;
12+
#writingCache = [];
13+
14+
constructor(connectionName = moment().valueOf().toString()) {
15+
this.#subPath = this.#pathSafe(connectionName);
16+
this.#basePath = os.homedir() + "/mqtt5-explorer-logs";
17+
}
18+
19+
get logsFolder() {
20+
return this.#basePath + this.#separator + this.#subPath;
21+
}
22+
23+
enqueue({ topic, payload, properties }) {
24+
const now = moment();
25+
this.#writingCache.unshift({
26+
topic,
27+
payload,
28+
properties: properties ? JSON.stringify(properties) : "",
29+
when: now.valueOf(),
30+
date: now.format("YYYY-MM-DD HH:mm:ss.SSS"),
31+
});
32+
}
33+
34+
async startLogging() {
35+
this.#writing = true;
36+
this.#subPath += this.#separator + moment().format("YYYY-MM-DD_HH-mm-ss");
37+
38+
// Create folders path
39+
fs.mkdirSync(this.logsFolder, { recursive: true });
40+
41+
while (this.#writing) {
42+
if (this.#writingCache.length === 0) {
43+
await this.#sleep(1000);
44+
continue;
45+
}
46+
47+
const { topic, payload, properties, when, date } =
48+
this.#writingCache.pop();
49+
const fileName =
50+
this.logsFolder + this.#separator + this.#pathSafe(topic) + ".csv";
51+
52+
// Create file if not exists
53+
if (!fs.existsSync(fileName)) {
54+
fs.writeFileSync(fileName, "Timestamp;Date;Value;Properties\n", {
55+
flag: "w",
56+
});
57+
}
58+
59+
// Update log file
60+
fs.writeFileSync(fileName, `${when};${date};${payload};${properties}\n`, {
61+
flag: "a",
62+
});
63+
}
64+
}
65+
66+
stopLogging() {
67+
this.#writing = false;
68+
this.#writingCache = [];
69+
}
70+
71+
#pathSafe = (topic) => {
72+
return topic?.replace(/\//g, "_") || "unknown-topic";
73+
};
74+
75+
#sleep = (ms) => {
76+
return new Promise((resolve) => setTimeout(resolve, ms));
77+
};
78+
}
79+
80+
export default MessageLogger;

src/views/MqttViewer.vue

+68-16
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@
125125
v-on="on"
126126
icon
127127
>
128-
<v-icon>mdi-bell-cog-outline</v-icon>
128+
<v-icon>mdi-message-badge-outline</v-icon>
129129
</v-btn>
130130
</template>
131-
<span>Enable notifications</span>
131+
<span>Notifications and logging</span>
132132
</v-tooltip>
133133
</div>
134134
<div v-if="selectedId !== -1" center-vertical class="ms-2">
@@ -470,7 +470,7 @@
470470

471471
<v-dialog v-model="notifySettings" max-width="70ch" persistent scrollable>
472472
<v-card>
473-
<v-card-title>Notifications</v-card-title>
473+
<v-card-title>Notifications and logging</v-card-title>
474474
<v-card-text class="d-flex flex-column">
475475
<div class="d-flex align-center">
476476
<v-text-field
@@ -540,7 +540,7 @@
540540
</div>
541541
<div class="mt-2">
542542
<div class="d-flex justify-space-between align-center">
543-
<span>Notification conditions:</span>
543+
<span>Trigger conditions:</span>
544544
<v-btn-toggle v-model="notifyJoinType">
545545
<v-btn
546546
v-bind:value="joinModes.OR"
@@ -618,9 +618,31 @@
618618
</div>
619619
</v-list>
620620
</div>
621+
<div class="mt-2 d-flex align-center justify-space-between">
622+
<v-switch
623+
v-model="notifySwitch"
624+
label="Enable notifications"
625+
inset
626+
/>
627+
<v-switch
628+
v-model="fileLoggingSwitch"
629+
label="Enable file logging"
630+
inset
631+
/>
632+
</div>
633+
<v-slide-y-transition>
634+
<div v-if="fileLoggingSwitch">
635+
<v-text-field
636+
v-model="messageLogger.logsFolder"
637+
v-bind:outlined="outline"
638+
label="Logs folder location"
639+
readonly
640+
/>
641+
</div>
642+
</v-slide-y-transition>
621643
</v-card-text>
622644
<v-card-actions>
623-
<v-btn v-on:click="notifyEntries = []" color="error" text>
645+
<v-btn v-on:click="resetNotifyAndLogging" color="error" text>
624646
Reset
625647
</v-btn>
626648
<v-spacer />
@@ -769,6 +791,7 @@ import Connection from "../utils/Connection";
769791
import ConnectionProperties from "../models/ConnectionProperties";
770792
import SearchEngine from "../utils/SearchEngine";
771793
import TreeNode from "../models/TreeNode";
794+
import MessageLogger from "../utils/MessageLogger";
772795
import { ipcRenderer } from "electron";
773796
774797
export default {
@@ -794,6 +817,8 @@ export default {
794817
searchModes: SearchEngine.modes,
795818
searchQuery: SearchEngine.QUERY,
796819
notifySettings: false,
820+
notifySwitch: false,
821+
fileLoggingSwitch: false,
797822
notifyFilterType: undefined,
798823
notifyEntry: undefined,
799824
notifyEntries: [],
@@ -802,6 +827,7 @@ export default {
802827
OR: "or",
803828
AND: "and",
804829
},
830+
messageLogger: undefined,
805831
}),
806832
807833
computed: {
@@ -839,6 +865,15 @@ export default {
839865
},
840866
},
841867
868+
watch: {
869+
fileLoggingSwitch(newValue, oldValue) {
870+
if (oldValue === newValue) return;
871+
872+
if (newValue) this.messageLogger?.startLogging();
873+
else this.messageLogger?.stopLogging();
874+
},
875+
},
876+
842877
beforeMount() {
843878
this.connectionProperties.init(
844879
this.$store.getters.getConnectionByIndex(this.$route.params.index)
@@ -850,6 +885,8 @@ export default {
850885
() => this.treeData.length
851886
);
852887
888+
this.messageLogger = new MessageLogger(this.connectionProperties.name);
889+
853890
ipcRenderer.send("enterViewerPage");
854891
ipcRenderer.on("searchPressed", this.toggleSearchField);
855892
},
@@ -863,6 +900,7 @@ export default {
863900
},
864901
865902
beforeDestroy() {
903+
this.messageLogger?.stopLogging();
866904
ipcRenderer.removeListener("searchPressed", this.toggleSearchField);
867905
},
868906
@@ -965,7 +1003,7 @@ export default {
9651003
}
9661004
9671005
if (!toDelete) {
968-
this.notify(node);
1006+
this.notifyAndWrite(node);
9691007
}
9701008
9711009
return toDelete;
@@ -999,10 +1037,15 @@ export default {
9991037
return "mdi-equal";
10001038
}
10011039
},
1040+
resetNotifyAndLogging() {
1041+
this.notifySwitch = false;
1042+
this.fileLoggingSwitch = false;
1043+
this.notifyEntries = [];
1044+
},
10021045
deleteNotifyEntry(entry) {
10031046
this.notifyEntries = this.notifyEntries.filter((item) => item !== entry);
10041047
},
1005-
notify(node) {
1048+
notifyAndWrite(node) {
10061049
let foundNodes = this.notifyEntries.flatMap((entry) => {
10071050
return node.deepSearch(entry.notifyEntry, entry.filterType);
10081051
});
@@ -1022,15 +1065,24 @@ export default {
10221065
10231066
foundNodes.forEach((foundNode) => {
10241067
if (!foundNode?.value) return;
1025-
this.sendNotification(
1026-
foundNode.value.topic,
1027-
foundNode.value.payload,
1028-
() => {
1029-
// Open the app if closed/minimized and select the topic
1030-
this.getProperties(foundNode);
1031-
ipcRenderer.send("focusWindow");
1032-
}
1033-
);
1068+
1069+
if (this.fileLoggingSwitch) {
1070+
// Write entry to file
1071+
this.messageLogger.enqueue(foundNode.value);
1072+
}
1073+
1074+
if (this.notifySwitch) {
1075+
// Send a system notification
1076+
this.sendNotification(
1077+
foundNode.value.topic,
1078+
foundNode.value.payload,
1079+
() => {
1080+
// Open the app if closed/minimized and select the topic
1081+
this.getProperties(foundNode);
1082+
ipcRenderer.send("focusWindow");
1083+
}
1084+
);
1085+
}
10341086
});
10351087
},
10361088
publishItem() {

0 commit comments

Comments
 (0)