@theotherwillembotha/node-red-loki 0.0.55 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -27
- package/build/GenerateNodes.js +6 -0
- package/build/Nodes.html +1071 -29
- package/build/Nodes.js +4 -0
- package/build/Plugins.js +2 -3
- package/build/icons/loki.png +0 -0
- package/build/index.js +3 -0
- package/build/loki/node/LokiLoggerConfigNode.js +7 -8
- package/build/loki/node/LokiQueryNode.js +128 -0
- package/build/loki/node/LokiServiceConfigNode.js +52 -0
- package/build/loki/service/LokiService.js +43 -0
- package/documentation/LokiLoggerConfigNode.png +0 -0
- package/documentation/LokiQueryNode.png +0 -0
- package/documentation/LokiServiceNode.png +0 -0
- package/documentation/example01.png +0 -0
- package/package.json +8 -5
package/build/Nodes.js
CHANGED
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
"use strict";
|
|
11
11
|
const NodeManager = require("@theotherwillembotha/node-red-plugincore").NodeManager;
|
|
12
|
+
const LokiServiceConfigNode = require("@theotherwillembotha/node-red-loki").LokiServiceConfigNode;
|
|
12
13
|
const LokiLoggerConfigNode = require("@theotherwillembotha/node-red-loki").LokiLoggerConfigNode;
|
|
14
|
+
const LokiQueryNode = require("@theotherwillembotha/node-red-loki").LokiQueryNode;
|
|
13
15
|
module.exports = (RED) => {
|
|
14
16
|
let manager = new NodeManager(RED);
|
|
17
|
+
manager.registerNodeType("LokiServiceConfigNode", LokiServiceConfigNode);
|
|
15
18
|
manager.registerNodeType("LokiLoggerConfigNode", LokiLoggerConfigNode);
|
|
19
|
+
manager.registerNodeType("LokiQueryNode", LokiQueryNode);
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
|
package/build/Plugins.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
"use strict";
|
|
11
11
|
const runtime = require("node-red").runtime;
|
|
12
|
-
|
|
12
|
+
const LokiService = require("@theotherwillembotha/node-red-loki").LokiService;
|
|
13
13
|
|
|
14
14
|
module.exports = function (RED) {
|
|
15
15
|
|
|
16
16
|
// 1. make a list of all the plugins that need to be installed.
|
|
17
|
-
let pluginList = []
|
|
17
|
+
let pluginList = [LokiService]
|
|
18
18
|
.map(service => service.getServiceDescriptor())
|
|
19
19
|
.filter(plugin => !RED.plugins.get(plugin.id()));
|
|
20
20
|
|
|
@@ -26,7 +26,6 @@ module.exports = function (RED) {
|
|
|
26
26
|
let plugin = RED.plugins.get(pluginID);
|
|
27
27
|
|
|
28
28
|
if(addedPlugin && plugin.instantiate && !plugin.instance){
|
|
29
|
-
console.log("Deploying instance of: " + plugin.id);
|
|
30
29
|
plugin.instance = new plugin.class();
|
|
31
30
|
|
|
32
31
|
// instantiate the plugin.
|
|
Binary file
|
package/build/index.js
CHANGED
|
@@ -14,4 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./loki/service/LokiService"), exports);
|
|
18
|
+
__exportStar(require("./loki/node/LokiServiceConfigNode"), exports);
|
|
17
19
|
__exportStar(require("./loki/node/LokiLoggerConfigNode"), exports);
|
|
20
|
+
__exportStar(require("./loki/node/LokiQueryNode"), exports);
|
|
@@ -17,18 +17,19 @@ const node_red_plugincore_1 = require("@theotherwillembotha/node-red-plugincore"
|
|
|
17
17
|
const node_red_plugincore_2 = require("@theotherwillembotha/node-red-plugincore");
|
|
18
18
|
const winston_1 = require("winston");
|
|
19
19
|
const winston_loki_1 = __importDefault(require("winston-loki"));
|
|
20
|
+
const LokiServiceConfigNode_1 = require("./LokiServiceConfigNode");
|
|
20
21
|
let LokiLoggerConfigNode = class LokiLoggerConfigNode extends node_red_plugincore_1.LoggerConfigNode {
|
|
21
22
|
constructor(node, config) {
|
|
22
23
|
super(node, config);
|
|
24
|
+
const serviceNode = node_red_plugincore_1.NodeManager.RED.nodes.getNode(config.lokiservice).node();
|
|
23
25
|
const loggerConfig = {
|
|
24
26
|
id: this.id(),
|
|
25
27
|
type: "LOKI",
|
|
26
28
|
template: config.template,
|
|
27
29
|
level: config.level,
|
|
28
|
-
host:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
tenantid: config.loki_tenantid,
|
|
30
|
+
host: serviceNode.url(),
|
|
31
|
+
basicAuth: serviceNode.basicAuth(),
|
|
32
|
+
tenantid: serviceNode.tenantId(),
|
|
32
33
|
};
|
|
33
34
|
this._logger = new LokiLogger(loggerConfig);
|
|
34
35
|
}
|
|
@@ -44,7 +45,7 @@ exports.LokiLoggerConfigNode = LokiLoggerConfigNode = __decorate([
|
|
|
44
45
|
group: "config",
|
|
45
46
|
sourceFile: node_red_plugincore_2.SourceUtility.getSourcePath("/build/", "/src/") + "LokiLoggerConfigNode.html",
|
|
46
47
|
package: "@theotherwillembotha/node-red-loki",
|
|
47
|
-
dependencies: [node_red_plugincore_1.LoggerService],
|
|
48
|
+
dependencies: [node_red_plugincore_1.LoggerService, LokiServiceConfigNode_1.LokiServiceConfigNode],
|
|
48
49
|
tags: ["LoggerType"]
|
|
49
50
|
}),
|
|
50
51
|
__metadata("design:paramtypes", [Object, Object])
|
|
@@ -64,9 +65,7 @@ class LokiLogger extends node_red_plugincore_1.AbstractLogger {
|
|
|
64
65
|
format: winston_1.format.json(),
|
|
65
66
|
replaceTimestamp: true,
|
|
66
67
|
onConnectionError: (err) => console.error(`[LokiLogger] connection error (host: ${config.host}):`, err),
|
|
67
|
-
basicAuth:
|
|
68
|
-
? `${config.userid}:${config.authtoken}`
|
|
69
|
-
: undefined
|
|
68
|
+
basicAuth: config.basicAuth,
|
|
70
69
|
};
|
|
71
70
|
if (config.tenantid) {
|
|
72
71
|
transportConfig.headers["X-Scope-OrgID"] = config.tenantid;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.LokiQueryNode = void 0;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const node_red_plugincore_1 = require("@theotherwillembotha/node-red-plugincore");
|
|
18
|
+
const node_red_plugincore_2 = require("@theotherwillembotha/node-red-plugincore");
|
|
19
|
+
const LokiServiceConfigNode_1 = require("./LokiServiceConfigNode");
|
|
20
|
+
const MAX_LOG_LINES = 1000;
|
|
21
|
+
const MAX_RESPONSE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
22
|
+
/**
|
|
23
|
+
* Parses a time value into an RFC3339 string suitable for the Loki query_range API.
|
|
24
|
+
*
|
|
25
|
+
* Accepts:
|
|
26
|
+
* - Nanosecond epoch integer strings (e.g. "1700000000000000000")
|
|
27
|
+
* - Relative durations (e.g. "1h", "30m", "7d", "2w")
|
|
28
|
+
* - RFC3339 / parseable dates (e.g. "2024-01-01T00:00:00Z")
|
|
29
|
+
*/
|
|
30
|
+
function parseTime(value) {
|
|
31
|
+
if (/^\d{16,}$/.test(value)) {
|
|
32
|
+
// Nanosecond epoch - convert to ms for Date
|
|
33
|
+
return new Date(Number(BigInt(value) / 1000000n)).toISOString();
|
|
34
|
+
}
|
|
35
|
+
const durationMatch = value.match(/^(\d+)(ms|s|m|h|d|w)$/i);
|
|
36
|
+
if (durationMatch) {
|
|
37
|
+
const n = parseInt(durationMatch[1]);
|
|
38
|
+
const unit = durationMatch[2].toLowerCase();
|
|
39
|
+
const msPerUnit = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000, w: 604800000 };
|
|
40
|
+
return new Date(Date.now() - n * msPerUnit[unit]).toISOString();
|
|
41
|
+
}
|
|
42
|
+
return new Date(value).toISOString();
|
|
43
|
+
}
|
|
44
|
+
let LokiQueryNode = class LokiQueryNode extends node_red_plugincore_1.BaseNode {
|
|
45
|
+
constructor(node, config) {
|
|
46
|
+
super(node, config);
|
|
47
|
+
this._serviceNode = node_red_plugincore_1.NodeManager.RED.nodes.getNode(config.lokiservice).node();
|
|
48
|
+
}
|
|
49
|
+
async onInput(message, _errorHandler) {
|
|
50
|
+
const query = message.query ?? this.config().query;
|
|
51
|
+
const startRaw = message.start ?? this.config().start;
|
|
52
|
+
const endRaw = message.end ?? this.config().end;
|
|
53
|
+
const endType = message.end ? undefined : this.config().end_type;
|
|
54
|
+
const start = parseTime(startRaw);
|
|
55
|
+
const end = (endType === "now" || (!endRaw && !message.end)) ? new Date().toISOString() : parseTime(endRaw);
|
|
56
|
+
const outputpath = this.config().outputpath || "payload";
|
|
57
|
+
const batchMode = this.config().batchMode === true;
|
|
58
|
+
const url = this._serviceNode.url().replace(/\/$/, "") + "/loki/api/v1/query_range";
|
|
59
|
+
try {
|
|
60
|
+
const response = await axios_1.default.get(url, {
|
|
61
|
+
headers: this._serviceNode.headers(),
|
|
62
|
+
params: { query, start, end, limit: MAX_LOG_LINES },
|
|
63
|
+
maxContentLength: MAX_RESPONSE_BYTES,
|
|
64
|
+
maxBodyLength: MAX_RESPONSE_BYTES,
|
|
65
|
+
});
|
|
66
|
+
const streams = response.data?.data?.result ?? [];
|
|
67
|
+
if (batchMode) {
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const stream of streams) {
|
|
70
|
+
const labels = stream.stream ?? {};
|
|
71
|
+
for (const [ts_ns, line] of (stream.values ?? [])) {
|
|
72
|
+
results.push({
|
|
73
|
+
timestamp: new Date(Number(BigInt(ts_ns) / 1000000n)).toISOString(),
|
|
74
|
+
line,
|
|
75
|
+
labels,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const out = { ...message };
|
|
80
|
+
out[outputpath] = results;
|
|
81
|
+
this.node().send(out);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const stream of streams) {
|
|
85
|
+
const labels = stream.stream ?? {};
|
|
86
|
+
for (const [ts_ns, line] of (stream.values ?? [])) {
|
|
87
|
+
const out = { ...message };
|
|
88
|
+
out[outputpath] = {
|
|
89
|
+
timestamp: new Date(Number(BigInt(ts_ns) / 1000000n)).toISOString(),
|
|
90
|
+
line,
|
|
91
|
+
labels,
|
|
92
|
+
};
|
|
93
|
+
this.node().send(out);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
_errorHandler(e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
exports.LokiQueryNode = LokiQueryNode;
|
|
104
|
+
__decorate([
|
|
105
|
+
(0, node_red_plugincore_2.Logger)(),
|
|
106
|
+
__metadata("design:type", node_red_plugincore_2.Log)
|
|
107
|
+
], LokiQueryNode.prototype, "log", void 0);
|
|
108
|
+
__decorate([
|
|
109
|
+
(0, node_red_plugincore_1.onInput)(),
|
|
110
|
+
__metadata("design:type", Function),
|
|
111
|
+
__metadata("design:paramtypes", [Object, Function]),
|
|
112
|
+
__metadata("design:returntype", Promise)
|
|
113
|
+
], LokiQueryNode.prototype, "onInput", null);
|
|
114
|
+
exports.LokiQueryNode = LokiQueryNode = __decorate([
|
|
115
|
+
(0, node_red_plugincore_1.NodeDescription)({
|
|
116
|
+
id: "LokiQueryNode",
|
|
117
|
+
name: "Loki Query",
|
|
118
|
+
group: "loki",
|
|
119
|
+
sourceFile: node_red_plugincore_1.SourceUtility.getSourcePath("/build/", "/src/") + "LokiQueryNode.html",
|
|
120
|
+
package: "@theotherwillembotha/node-red-loki",
|
|
121
|
+
templates: [
|
|
122
|
+
{ template: node_red_plugincore_2.LoggerTemplate, config: {} }
|
|
123
|
+
],
|
|
124
|
+
dependencies: [LokiServiceConfigNode_1.LokiServiceConfigNode],
|
|
125
|
+
tags: ["Loki"]
|
|
126
|
+
}),
|
|
127
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
128
|
+
], LokiQueryNode);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.LokiServiceConfigNode = void 0;
|
|
13
|
+
const node_red_plugincore_1 = require("@theotherwillembotha/node-red-plugincore");
|
|
14
|
+
let LokiServiceConfigNode = class LokiServiceConfigNode extends node_red_plugincore_1.ConfigNode {
|
|
15
|
+
constructor(node, config) {
|
|
16
|
+
super(node, config);
|
|
17
|
+
}
|
|
18
|
+
url() {
|
|
19
|
+
return this.config().url;
|
|
20
|
+
}
|
|
21
|
+
basicAuth() {
|
|
22
|
+
const { userid, authtoken } = this.config();
|
|
23
|
+
return (userid && authtoken) ? `${userid}:${authtoken}` : undefined;
|
|
24
|
+
}
|
|
25
|
+
tenantId() {
|
|
26
|
+
return this.config().tenantid || undefined;
|
|
27
|
+
}
|
|
28
|
+
headers() {
|
|
29
|
+
const h = {};
|
|
30
|
+
const auth = this.basicAuth();
|
|
31
|
+
if (auth) {
|
|
32
|
+
h['Authorization'] = 'Basic ' + Buffer.from(auth).toString('base64');
|
|
33
|
+
}
|
|
34
|
+
const tenant = this.tenantId();
|
|
35
|
+
if (tenant) {
|
|
36
|
+
h['X-Scope-OrgID'] = tenant;
|
|
37
|
+
}
|
|
38
|
+
return h;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
exports.LokiServiceConfigNode = LokiServiceConfigNode;
|
|
42
|
+
exports.LokiServiceConfigNode = LokiServiceConfigNode = __decorate([
|
|
43
|
+
(0, node_red_plugincore_1.NodeDescription)({
|
|
44
|
+
id: "LokiServiceConfigNode",
|
|
45
|
+
name: "Loki Service",
|
|
46
|
+
group: "config",
|
|
47
|
+
sourceFile: node_red_plugincore_1.SourceUtility.getSourcePath("/build/", "/src/") + "LokiServiceConfigNode.html",
|
|
48
|
+
package: "@theotherwillembotha/node-red-loki",
|
|
49
|
+
tags: ["Loki"]
|
|
50
|
+
}),
|
|
51
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
52
|
+
], LokiServiceConfigNode);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LokiService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const node_red_plugincore_1 = require("@theotherwillembotha/node-red-plugincore");
|
|
9
|
+
class LokiService extends node_red_plugincore_1.BaseService {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("LokiService");
|
|
12
|
+
}
|
|
13
|
+
init(red) {
|
|
14
|
+
this.red = red;
|
|
15
|
+
this.red.httpAdmin.post("/lokiservice/testconnection", this.red.auth.needsPermission("inject.write"), async (request, response) => {
|
|
16
|
+
const { url, userid, authtoken, tenantid } = request.body;
|
|
17
|
+
try {
|
|
18
|
+
const headers = {};
|
|
19
|
+
if (userid && authtoken) {
|
|
20
|
+
headers['Authorization'] = 'Basic ' + Buffer.from(`${userid}:${authtoken}`).toString('base64');
|
|
21
|
+
}
|
|
22
|
+
if (tenantid) {
|
|
23
|
+
headers['X-Scope-OrgID'] = tenantid;
|
|
24
|
+
}
|
|
25
|
+
await axios_1.default.get(url.replace(/\/$/, '') + '/loki/api/v1/labels', {
|
|
26
|
+
headers,
|
|
27
|
+
timeout: 5000,
|
|
28
|
+
});
|
|
29
|
+
response.json({ success: true });
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
const message = e.response?.data?.message ?? e.response?.data ?? e.message;
|
|
33
|
+
response.json({ success: false, error: String(message) });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
deinit(_red) { }
|
|
38
|
+
async onDeploy(_flowDeployment) { }
|
|
39
|
+
static getServiceDescriptor() {
|
|
40
|
+
return new node_red_plugincore_1.ServiceDescriptor("@theotherwillembotha/lokiservice", "LokiService", "services-plugin", "@theotherwillembotha/node-red-loki", LokiService, []);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.LokiService = LokiService;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theotherwillembotha/node-red-loki",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Grafana Loki
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Grafana Loki integration for Node-RED ( service config, logger, and LogQL query node). Built on @theotherwillembotha/node-red-plugincore.",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"exports": "./build/index.js",
|
|
7
7
|
"author": "Willem Botha (@theotherwillembotha)",
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
18
|
-
"build": "npm run clean && tsc && npm run generate-nodes",
|
|
18
|
+
"build": "npm run clean && tsc && npm run generate-nodes && npm run copy-files",
|
|
19
19
|
"clean": "rm -rf ./build",
|
|
20
|
-
"generate-nodes": "node build/GenerateNodes.js"
|
|
20
|
+
"generate-nodes": "node build/GenerateNodes.js",
|
|
21
|
+
"copy-files": "cp -r ./icons ./build/"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"build/",
|
|
25
|
+
"documentation/",
|
|
24
26
|
"LICENSE",
|
|
25
27
|
"README.md"
|
|
26
28
|
],
|
|
@@ -37,7 +39,8 @@
|
|
|
37
39
|
"nodered": ">=4.0.0"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@theotherwillembotha/node-red-plugincore": "^0.0
|
|
42
|
+
"@theotherwillembotha/node-red-plugincore": "^0.1.0",
|
|
43
|
+
"axios": "^1.7.0",
|
|
41
44
|
"winston": "^3.17.0",
|
|
42
45
|
"winston-loki": "^6.1.3"
|
|
43
46
|
},
|