@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.html
CHANGED
|
@@ -8,6 +8,128 @@
|
|
|
8
8
|
|
|
9
9
|
-->
|
|
10
10
|
|
|
11
|
+
<script type="text/javascript">
|
|
12
|
+
RED.nodes.registerType('LokiServiceConfigNode', {
|
|
13
|
+
category: 'config',
|
|
14
|
+
label: function() {
|
|
15
|
+
return this.name
|
|
16
|
+
},
|
|
17
|
+
paletteLabel: 'Loki Service',
|
|
18
|
+
defaults: {
|
|
19
|
+
name: {
|
|
20
|
+
value: 'Loki Service',
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
url: {
|
|
24
|
+
value: 'http://localhost:3100',
|
|
25
|
+
required: true
|
|
26
|
+
},
|
|
27
|
+
userid: {
|
|
28
|
+
required: false
|
|
29
|
+
},
|
|
30
|
+
authtoken: {
|
|
31
|
+
required: false
|
|
32
|
+
},
|
|
33
|
+
tenantid: {
|
|
34
|
+
required: false
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
oneditprepare: function() {
|
|
38
|
+
// **** LokiServiceConfigNode **** //
|
|
39
|
+
{
|
|
40
|
+
$("#loki-service-test-btn").on("click", function() {
|
|
41
|
+
let $btn = $(this);
|
|
42
|
+
let $status = $("#loki-service-test-status");
|
|
43
|
+
$btn.prop("disabled", true);
|
|
44
|
+
$status.text("Testing...").css("color", "gray");
|
|
45
|
+
$.ajax({
|
|
46
|
+
url: "lokiservice/testconnection",
|
|
47
|
+
type: "POST",
|
|
48
|
+
contentType: "application/json; charset=utf-8",
|
|
49
|
+
data: JSON.stringify({
|
|
50
|
+
url: $("#node-config-input-url").val(),
|
|
51
|
+
userid: $("#node-config-input-userid").val(),
|
|
52
|
+
authtoken: $("#node-config-input-authtoken").val(),
|
|
53
|
+
tenantid: $("#node-config-input-tenantid").val(),
|
|
54
|
+
}),
|
|
55
|
+
success: function(response) {
|
|
56
|
+
if (response.success) {
|
|
57
|
+
$status.text("\u2714 Connected").css("color", "green");
|
|
58
|
+
} else {
|
|
59
|
+
let detail = response.error ? " \u2014 " + response.error : "";
|
|
60
|
+
$status.text("\u2718 Failed" + detail).css("color", "red");
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
error: function(jqXHR, textStatus, errorThrown) {
|
|
64
|
+
$status.text("\u2718 Failed \u2014 " + errorThrown).css("color", "red");
|
|
65
|
+
},
|
|
66
|
+
complete: function() {
|
|
67
|
+
$btn.prop("disabled", false);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
oneditsave: function() {
|
|
74
|
+
// **** LokiServiceConfigNode **** //
|
|
75
|
+
{}
|
|
76
|
+
},
|
|
77
|
+
oneditcancel: function() {},
|
|
78
|
+
oneditdelete: function() {},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
<script type="text/html" data-template-name='LokiServiceConfigNode'>
|
|
85
|
+
<div id='section_LokiServiceConfigNode'>
|
|
86
|
+
<div class="form-row">
|
|
87
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
88
|
+
<input type="text" id="node-config-input-name" />
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="form-row">
|
|
92
|
+
<label for="node-config-input-url"><i class="fa fa-server"></i> URL</label>
|
|
93
|
+
<input type="text" id="node-config-input-url" placeholder="http://localhost:3100" />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-config-input-tenantid"><i class="fa fa-building-o"></i> Tenant ID</label>
|
|
98
|
+
<input type="text" id="node-config-input-tenantid" placeholder="(optional)" />
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="form-row">
|
|
102
|
+
<label for="node-config-input-userid"><i class="fa fa-user"></i> Username</label>
|
|
103
|
+
<input type="text" id="node-config-input-userid" placeholder="(optional)" />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label for="node-config-input-authtoken"><i class="fa fa-key"></i> Auth Token</label>
|
|
108
|
+
<input type="password" id="node-config-input-authtoken" placeholder="(optional)" />
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="form-row" style="margin-top:12px;">
|
|
112
|
+
<label></label>
|
|
113
|
+
<button id="loki-service-test-btn" type="button" class="red-ui-button">
|
|
114
|
+
<i class="fa fa-plug"></i> Test Connection
|
|
115
|
+
</button>
|
|
116
|
+
<span id="loki-service-test-status" style="margin-left:10px; font-size:12px;"></span>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
</div>
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<script type="text/markdown" data-help-name='LokiServiceConfigNode'>
|
|
123
|
+
Shared connection configuration for a Grafana Loki instance. Referenced by the Loki Logger and Loki Query nodes.
|
|
124
|
+
|
|
125
|
+
### Properties
|
|
126
|
+
|
|
127
|
+
: *name* (string) : Display label for this config node.
|
|
128
|
+
: *url* (string) : Base URL of the Loki instance, including port (e.g. `http://localhost:3100`).
|
|
129
|
+
: *tenant ID* (string) : Tenant identifier for multi-tenant deployments (`X-Scope-OrgID` header). Leave blank for single-tenant setups.
|
|
130
|
+
: *username* (string) : Username for basic authentication. Leave blank if auth is not required.
|
|
131
|
+
: *auth token* (string) : Password or token for basic authentication.
|
|
132
|
+
</script>
|
|
11
133
|
<script type="text/javascript">
|
|
12
134
|
RED.nodes.registerType('LokiLoggerConfigNode', {
|
|
13
135
|
category: 'config',
|
|
@@ -28,10 +150,10 @@
|
|
|
28
150
|
value: 'info',
|
|
29
151
|
required: true
|
|
30
152
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
153
|
+
lokiservice: {
|
|
154
|
+
type: 'LokiServiceConfigNode',
|
|
155
|
+
required: true
|
|
156
|
+
},
|
|
35
157
|
},
|
|
36
158
|
oneditprepare: function() {
|
|
37
159
|
// **** LokiLoggerConfigNode **** //
|
|
@@ -94,20 +216,8 @@
|
|
|
94
216
|
</div>
|
|
95
217
|
|
|
96
218
|
<div class="form-row">
|
|
97
|
-
<label class="towb_editorlabel" for="node-config-input-
|
|
98
|
-
<input
|
|
99
|
-
</div>
|
|
100
|
-
<div class="form-row">
|
|
101
|
-
<label class="towb_editorlabel" for="node-config-input-loki_tenantid"><i class="fa fa-building-o"></i> Tenant ID</label>
|
|
102
|
-
<input class="" type="text" id="node-config-input-loki_tenantid" />
|
|
103
|
-
</div>
|
|
104
|
-
<div class="form-row">
|
|
105
|
-
<label class="towb_editorlabel" for="node-config-input-loki_userid"><i class="fa fa-user"></i> Username</label>
|
|
106
|
-
<input class="" type="text" id="node-config-input-loki_userid" />
|
|
107
|
-
</div>
|
|
108
|
-
<div class="form-row">
|
|
109
|
-
<label class="towb_editorlabel" for="node-config-input-loki_authtoken"><i class="fa fa-key"></i> Auth Token</label>
|
|
110
|
-
<input class="" type="password" id="node-config-input-loki_authtoken" />
|
|
219
|
+
<label class="towb_editorlabel" for="node-config-input-lokiservice"><i class="fa fa-server"></i> Loki Service</label>
|
|
220
|
+
<input type="text" id="node-config-input-lokiservice" />
|
|
111
221
|
</div>
|
|
112
222
|
|
|
113
223
|
<div class="form-row">
|
|
@@ -123,13 +233,10 @@
|
|
|
123
233
|
Pushes log messages to a Grafana Loki instance using the Loki HTTP API.
|
|
124
234
|
|
|
125
235
|
### Properties
|
|
126
|
-
: *name*
|
|
127
|
-
: *
|
|
128
|
-
: *
|
|
129
|
-
: *
|
|
130
|
-
: *username* (string) : Username for Loki authentication.
|
|
131
|
-
: *auth token* (string) : Authentication token or password for Loki.
|
|
132
|
-
: *template* (mustache) : A Mustache/Handlebars template that formats the log message body.
|
|
236
|
+
: *name* (string) : A descriptive name for this logger configuration.
|
|
237
|
+
: *loki service* (config) : The Loki Service config node that provides the connection URL and credentials.
|
|
238
|
+
: *level* (level) : The minimum log level. Messages below this threshold are suppressed. Options: DEBUG, INFO, WARNING, ERROR.
|
|
239
|
+
: *template* (mustache) : A Mustache/Handlebars template that formats the log message body.
|
|
133
240
|
|
|
134
241
|
### Mustache Templating
|
|
135
242
|
Mustache/Handlebars templates build the message string pushed to Loki.
|
|
@@ -142,12 +249,947 @@ Mustache/Handlebars templates build the message string pushed to Loki.
|
|
|
142
249
|
: *json* (helper) : Serialises an object to a JSON string using `JSON.stringify`.
|
|
143
250
|
|
|
144
251
|
#### Examples
|
|
145
|
-
- `{{{json msg}}}`
|
|
252
|
+
- `{{{json msg}}}` - serialises the full message object as JSON.
|
|
146
253
|
|
|
147
254
|
#### References
|
|
148
|
-
- [Grafana Loki
|
|
149
|
-
- [Mustache
|
|
150
|
-
- [Mustache
|
|
255
|
+
- [Grafana Loki - HTTP API](https://grafana.com/docs/loki/latest/reference/loki-http-api/)
|
|
256
|
+
- [Mustache - Wikipedia](https://en.wikipedia.org/wiki/Mustache_(template_system))
|
|
257
|
+
- [Mustache - Playground](https://jgonggrijp.gitlab.io/wontache/playground.html)
|
|
258
|
+
</script>
|
|
259
|
+
<!-- LokiQueryNode -->
|
|
260
|
+
<script type="text/javascript">
|
|
261
|
+
window.PluginCore = window.PluginCore || {};
|
|
262
|
+
/**
|
|
263
|
+
* PluginCore.createLogQLEditor(elementId, initialValue)
|
|
264
|
+
*
|
|
265
|
+
* Creates a Monaco editor pre-configured with the LogQL language:
|
|
266
|
+
* syntax highlighting for stream selectors, pipeline stages, metric
|
|
267
|
+
* functions, and aggregation operators; plus snippet completions.
|
|
268
|
+
*
|
|
269
|
+
* The LogQL language is registered with Monaco lazily on first call.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} elementId - DOM id of the container element.
|
|
272
|
+
* @param {string} initialValue - Starting content of the editor.
|
|
273
|
+
* @returns {{ getValue(): string, dispose(): void \}}
|
|
274
|
+
*/
|
|
275
|
+
PluginCore.createLogQLEditor = function(elementId, initialValue) {
|
|
276
|
+
// Register the LogQL language with Monaco once per session.
|
|
277
|
+
if (!PluginCore._logqlReady) {
|
|
278
|
+
PluginCore._logqlReady = true;
|
|
279
|
+
monaco.languages.register({
|
|
280
|
+
id: 'logql'
|
|
281
|
+
});
|
|
282
|
+
monaco.languages.setLanguageConfiguration('logql', {
|
|
283
|
+
brackets: [
|
|
284
|
+
['{', '}'],
|
|
285
|
+
['[', ']'],
|
|
286
|
+
['(', ')']
|
|
287
|
+
],
|
|
288
|
+
autoClosingPairs: [{
|
|
289
|
+
open: '{',
|
|
290
|
+
close: '}'
|
|
291
|
+
}, {
|
|
292
|
+
open: '[',
|
|
293
|
+
close: ']'
|
|
294
|
+
}, {
|
|
295
|
+
open: '(',
|
|
296
|
+
close: ')'
|
|
297
|
+
}, {
|
|
298
|
+
open: '"',
|
|
299
|
+
close: '"'
|
|
300
|
+
}, {
|
|
301
|
+
open: "'",
|
|
302
|
+
close: "'"
|
|
303
|
+
}, {
|
|
304
|
+
open: '`',
|
|
305
|
+
close: '`'
|
|
306
|
+
}],
|
|
307
|
+
surroundingPairs: [{
|
|
308
|
+
open: '{',
|
|
309
|
+
close: '}'
|
|
310
|
+
}, {
|
|
311
|
+
open: '[',
|
|
312
|
+
close: ']'
|
|
313
|
+
}, {
|
|
314
|
+
open: '(',
|
|
315
|
+
close: ')'
|
|
316
|
+
}, {
|
|
317
|
+
open: '"',
|
|
318
|
+
close: '"'
|
|
319
|
+
}, {
|
|
320
|
+
open: "'",
|
|
321
|
+
close: "'"
|
|
322
|
+
}]
|
|
323
|
+
});
|
|
324
|
+
monaco.languages.setMonarchTokensProvider('logql', {
|
|
325
|
+
defaultToken: '',
|
|
326
|
+
tokenPostfix: '.logql',
|
|
327
|
+
keywords: ['by', 'without', 'on', 'ignoring', 'group_left', 'group_right', 'bool', 'or', 'and', 'unless', 'offset'],
|
|
328
|
+
metricFns: ['rate', 'count_over_time', 'bytes_rate', 'bytes_over_time', 'absent_over_time', 'sum_over_time', 'avg_over_time', 'min_over_time', 'max_over_time', 'first_over_time', 'last_over_time', 'stddev_over_time', 'stdvar_over_time', 'quantile_over_time', 'label_replace', 'vector', 'sort', 'sort_desc'],
|
|
329
|
+
aggregations: ['sum', 'avg', 'min', 'max', 'count', 'stddev', 'stdvar', 'bottomk', 'topk'],
|
|
330
|
+
pipelineStages: ['json', 'logfmt', 'pattern', 'regexp', 'unpack', 'line_format', 'label_format', 'drop', 'keep', 'decolorize', 'unwrap', 'ip', 'duration', 'bytes', 'distinct'],
|
|
331
|
+
tokenizer: {
|
|
332
|
+
root: [
|
|
333
|
+
// Stream selector open brace
|
|
334
|
+
[/\{/, {
|
|
335
|
+
token: 'delimiter.curly',
|
|
336
|
+
bracket: '@open',
|
|
337
|
+
next: '@streamSelector'
|
|
338
|
+
}],
|
|
339
|
+
// Range vector e.g. [5m]
|
|
340
|
+
[/\[\d+(ms|s|m|h|d|w|y)\]/, 'number.duration'],
|
|
341
|
+
// Pipeline filter operators
|
|
342
|
+
[/[|!][=~]/, 'operator.filter'],
|
|
343
|
+
// Pipe
|
|
344
|
+
[/\|/, 'operator.pipe'],
|
|
345
|
+
// Comparison operators
|
|
346
|
+
[/[><=!]+/, 'operator'],
|
|
347
|
+
// Numbers
|
|
348
|
+
[/\d+(\.\d+)?/, 'number'],
|
|
349
|
+
// Identifiers -> map to keyword/function/aggregation/pipeline categories
|
|
350
|
+
[/[a-zA-Z_]\w*/, {
|
|
351
|
+
cases: {
|
|
352
|
+
'@keywords': 'keyword',
|
|
353
|
+
'@aggregations': 'type.aggregate',
|
|
354
|
+
'@metricFns': 'type.function',
|
|
355
|
+
'@pipelineStages': 'type.pipeline',
|
|
356
|
+
'@default': 'identifier'
|
|
357
|
+
}
|
|
358
|
+
}],
|
|
359
|
+
// String literals
|
|
360
|
+
[/"/, {
|
|
361
|
+
token: 'string.quote',
|
|
362
|
+
bracket: '@open',
|
|
363
|
+
next: '@dblString'
|
|
364
|
+
}],
|
|
365
|
+
[/'/, {
|
|
366
|
+
token: 'string.quote',
|
|
367
|
+
bracket: '@open',
|
|
368
|
+
next: '@sglString'
|
|
369
|
+
}],
|
|
370
|
+
[/`/, {
|
|
371
|
+
token: 'string.quote',
|
|
372
|
+
bracket: '@open',
|
|
373
|
+
next: '@rawString'
|
|
374
|
+
}]
|
|
375
|
+
],
|
|
376
|
+
streamSelector: [
|
|
377
|
+
[/\}/, {
|
|
378
|
+
token: 'delimiter.curly',
|
|
379
|
+
bracket: '@close',
|
|
380
|
+
next: '@pop'
|
|
381
|
+
}],
|
|
382
|
+
[/[a-zA-Z_]\w*/, 'attribute.name'],
|
|
383
|
+
[/=~|!=|=|!~/, 'operator'],
|
|
384
|
+
[/"/, {
|
|
385
|
+
token: 'string.quote',
|
|
386
|
+
bracket: '@open',
|
|
387
|
+
next: '@dblString'
|
|
388
|
+
}],
|
|
389
|
+
[/'/, {
|
|
390
|
+
token: 'string.quote',
|
|
391
|
+
bracket: '@open',
|
|
392
|
+
next: '@sglString'
|
|
393
|
+
}],
|
|
394
|
+
[/`/, {
|
|
395
|
+
token: 'string.quote',
|
|
396
|
+
bracket: '@open',
|
|
397
|
+
next: '@rawString'
|
|
398
|
+
}],
|
|
399
|
+
[/,/, 'delimiter']
|
|
400
|
+
],
|
|
401
|
+
dblString: [
|
|
402
|
+
[/[^"\\]+/, 'string'],
|
|
403
|
+
[/\\./, 'string.escape'],
|
|
404
|
+
[/"/, {
|
|
405
|
+
token: 'string.quote',
|
|
406
|
+
bracket: '@close',
|
|
407
|
+
next: '@pop'
|
|
408
|
+
}]
|
|
409
|
+
],
|
|
410
|
+
sglString: [
|
|
411
|
+
[/[^'\\]+/, 'string'],
|
|
412
|
+
[/\\./, 'string.escape'],
|
|
413
|
+
[/'/, {
|
|
414
|
+
token: 'string.quote',
|
|
415
|
+
bracket: '@close',
|
|
416
|
+
next: '@pop'
|
|
417
|
+
}]
|
|
418
|
+
],
|
|
419
|
+
rawString: [
|
|
420
|
+
[/[^`]+/, 'string'],
|
|
421
|
+
[/`/, {
|
|
422
|
+
token: 'string.quote',
|
|
423
|
+
bracket: '@close',
|
|
424
|
+
next: '@pop'
|
|
425
|
+
}]
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
monaco.languages.registerCompletionItemProvider('logql', {
|
|
430
|
+
triggerCharacters: ['{', '|', ' ', '('],
|
|
431
|
+
provideCompletionItems: function(model, position) {
|
|
432
|
+
var word = model.getWordUntilPosition(position);
|
|
433
|
+
var range = {
|
|
434
|
+
startLineNumber: position.lineNumber,
|
|
435
|
+
endLineNumber: position.lineNumber,
|
|
436
|
+
startColumn: word.startColumn,
|
|
437
|
+
endColumn: word.endColumn
|
|
438
|
+
};
|
|
439
|
+
var mk = monaco.languages.CompletionItemKind;
|
|
440
|
+
var SR = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
|
|
441
|
+
var suggestions = [];
|
|
442
|
+
// Stream selector template
|
|
443
|
+
suggestions.push({
|
|
444
|
+
label: '{label="value"}',
|
|
445
|
+
kind: mk.Snippet,
|
|
446
|
+
detail: 'Log stream selector',
|
|
447
|
+
documentation: 'Select log streams matching a set of label matchers.',
|
|
448
|
+
insertText: '{${1:app}="${2:nodered}"}',
|
|
449
|
+
insertTextRules: SR,
|
|
450
|
+
range: range
|
|
451
|
+
});
|
|
452
|
+
// Metric query functions
|
|
453
|
+
[{
|
|
454
|
+
l: 'rate',
|
|
455
|
+
d: 'Per-second rate of log entries',
|
|
456
|
+
s: 'rate({${1:app}="${2}"} [${3:5m}])'
|
|
457
|
+
}, {
|
|
458
|
+
l: 'count_over_time',
|
|
459
|
+
d: 'Count of log entries over time range',
|
|
460
|
+
s: 'count_over_time({${1:app}="${2}"} [${3:5m}])'
|
|
461
|
+
}, {
|
|
462
|
+
l: 'bytes_rate',
|
|
463
|
+
d: 'Per-second byte rate of log entries',
|
|
464
|
+
s: 'bytes_rate({${1:app}="${2}"} [${3:5m}])'
|
|
465
|
+
}, {
|
|
466
|
+
l: 'bytes_over_time',
|
|
467
|
+
d: 'Total bytes of log entries over range',
|
|
468
|
+
s: 'bytes_over_time({${1:app}="${2}"} [${3:5m}])'
|
|
469
|
+
}, {
|
|
470
|
+
l: 'absent_over_time',
|
|
471
|
+
d: 'Returns 1 if no entries found in range',
|
|
472
|
+
s: 'absent_over_time({${1:app}="${2}"} [${3:5m}])'
|
|
473
|
+
}, {
|
|
474
|
+
l: 'sum_over_time',
|
|
475
|
+
d: 'Sum of unwrapped values over range',
|
|
476
|
+
s: 'sum_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])'
|
|
477
|
+
}, {
|
|
478
|
+
l: 'avg_over_time',
|
|
479
|
+
d: 'Average of unwrapped values over range',
|
|
480
|
+
s: 'avg_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])'
|
|
481
|
+
}, {
|
|
482
|
+
l: 'min_over_time',
|
|
483
|
+
d: 'Minimum of unwrapped values over range',
|
|
484
|
+
s: 'min_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])'
|
|
485
|
+
}, {
|
|
486
|
+
l: 'max_over_time',
|
|
487
|
+
d: 'Maximum of unwrapped values over range',
|
|
488
|
+
s: 'max_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])'
|
|
489
|
+
}, {
|
|
490
|
+
l: 'quantile_over_time',
|
|
491
|
+
d: 'Quantile of unwrapped values',
|
|
492
|
+
s: 'quantile_over_time(${1:0.99}, {${2:app}="${3}"} | unwrap ${4:field} [${5:5m}])'
|
|
493
|
+
}, {
|
|
494
|
+
l: 'first_over_time',
|
|
495
|
+
d: 'First unwrapped value over range',
|
|
496
|
+
s: 'first_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])'
|
|
497
|
+
}, {
|
|
498
|
+
l: 'last_over_time',
|
|
499
|
+
d: 'Last unwrapped value over range',
|
|
500
|
+
s: 'last_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])'
|
|
501
|
+
}, {
|
|
502
|
+
l: 'label_replace',
|
|
503
|
+
d: 'Replace label value using regex',
|
|
504
|
+
s: 'label_replace(${1:expr}, "${2:dst_label}", "${3:$1}", "${4:src_label}", "${5:(.*)}")'
|
|
505
|
+
}].forEach(function(f) {
|
|
506
|
+
suggestions.push({
|
|
507
|
+
label: f.l,
|
|
508
|
+
kind: mk.Function,
|
|
509
|
+
detail: f.d,
|
|
510
|
+
insertText: f.s,
|
|
511
|
+
insertTextRules: SR,
|
|
512
|
+
range: range
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
// Aggregation operators
|
|
516
|
+
[{
|
|
517
|
+
l: 'sum',
|
|
518
|
+
d: 'Sum over dimensions'
|
|
519
|
+
}, {
|
|
520
|
+
l: 'avg',
|
|
521
|
+
d: 'Average over dimensions'
|
|
522
|
+
}, {
|
|
523
|
+
l: 'min',
|
|
524
|
+
d: 'Minimum over dimensions'
|
|
525
|
+
}, {
|
|
526
|
+
l: 'max',
|
|
527
|
+
d: 'Maximum over dimensions'
|
|
528
|
+
}, {
|
|
529
|
+
l: 'count',
|
|
530
|
+
d: 'Count over dimensions'
|
|
531
|
+
}, {
|
|
532
|
+
l: 'stddev',
|
|
533
|
+
d: 'Standard deviation'
|
|
534
|
+
}, {
|
|
535
|
+
l: 'stdvar',
|
|
536
|
+
d: 'Standard variance'
|
|
537
|
+
}, {
|
|
538
|
+
l: 'bottomk',
|
|
539
|
+
d: 'Smallest k elements',
|
|
540
|
+
s: 'bottomk(${1:5}, ${2:expr})'
|
|
541
|
+
}, {
|
|
542
|
+
l: 'topk',
|
|
543
|
+
d: 'Largest k elements',
|
|
544
|
+
s: 'topk(${1:5}, ${2:expr})'
|
|
545
|
+
}].forEach(function(a) {
|
|
546
|
+
var snippet = a.s || (a.l + '(${1:by (${2:label})}) (${3:expr})');
|
|
547
|
+
suggestions.push({
|
|
548
|
+
label: a.l,
|
|
549
|
+
kind: mk.Keyword,
|
|
550
|
+
detail: a.d,
|
|
551
|
+
insertText: snippet,
|
|
552
|
+
insertTextRules: SR,
|
|
553
|
+
range: range
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
// Pipeline stages
|
|
557
|
+
[{
|
|
558
|
+
l: '|= "..."',
|
|
559
|
+
s: '|= "${1}"',
|
|
560
|
+
d: 'Filter: line contains string'
|
|
561
|
+
}, {
|
|
562
|
+
l: '!= "..."',
|
|
563
|
+
s: '!= "${1}"',
|
|
564
|
+
d: 'Filter: line does not contain string'
|
|
565
|
+
}, {
|
|
566
|
+
l: '|~ "..."',
|
|
567
|
+
s: '|~ "${1}"',
|
|
568
|
+
d: 'Filter: line matches regex'
|
|
569
|
+
}, {
|
|
570
|
+
l: '!~ "..."',
|
|
571
|
+
s: '!~ "${1}"',
|
|
572
|
+
d: 'Filter: line does not match regex'
|
|
573
|
+
}, {
|
|
574
|
+
l: '| json',
|
|
575
|
+
s: '| json',
|
|
576
|
+
d: 'Parse JSON log line'
|
|
577
|
+
}, {
|
|
578
|
+
l: '| logfmt',
|
|
579
|
+
s: '| logfmt',
|
|
580
|
+
d: 'Parse logfmt log line'
|
|
581
|
+
}, {
|
|
582
|
+
l: '| pattern "..."',
|
|
583
|
+
s: '| pattern "${1:<_> level=<level> <_>}"',
|
|
584
|
+
d: 'Parse using pattern'
|
|
585
|
+
}, {
|
|
586
|
+
l: '| regexp "..."',
|
|
587
|
+
s: '| regexp "${1:(?P<level>[a-z]+)}"',
|
|
588
|
+
d: 'Parse using named regex groups'
|
|
589
|
+
}, {
|
|
590
|
+
l: '| line_format "..."',
|
|
591
|
+
s: '| line_format "${1:{{.level\}} {{.msg\}}}"',
|
|
592
|
+
d: 'Reformat the log line'
|
|
593
|
+
}, {
|
|
594
|
+
l: '| label_format',
|
|
595
|
+
s: '| label_format ${1:new_label}=${2:old_label}',
|
|
596
|
+
d: 'Rename or transform labels'
|
|
597
|
+
}, {
|
|
598
|
+
l: '| unwrap',
|
|
599
|
+
s: '| unwrap ${1:label}',
|
|
600
|
+
d: 'Extract numeric value for metric queries'
|
|
601
|
+
}, {
|
|
602
|
+
l: '| unpack',
|
|
603
|
+
s: '| unpack',
|
|
604
|
+
d: 'Unpack JSON log fields into labels'
|
|
605
|
+
}, {
|
|
606
|
+
l: '| drop',
|
|
607
|
+
s: '| drop ${1:label}',
|
|
608
|
+
d: 'Drop specified labels'
|
|
609
|
+
}, {
|
|
610
|
+
l: '| keep',
|
|
611
|
+
s: '| keep ${1:label}',
|
|
612
|
+
d: 'Keep only specified labels'
|
|
613
|
+
}, {
|
|
614
|
+
l: '| decolorize',
|
|
615
|
+
s: '| decolorize',
|
|
616
|
+
d: 'Strip ANSI colour codes'
|
|
617
|
+
}].forEach(function(p) {
|
|
618
|
+
suggestions.push({
|
|
619
|
+
label: p.l,
|
|
620
|
+
kind: mk.Operator,
|
|
621
|
+
detail: p.d,
|
|
622
|
+
insertText: p.s,
|
|
623
|
+
insertTextRules: SR,
|
|
624
|
+
range: range
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
// Keywords
|
|
628
|
+
['by', 'without', 'on', 'or', 'and', 'unless', 'bool', 'offset', 'ignoring'].forEach(function(kw) {
|
|
629
|
+
suggestions.push({
|
|
630
|
+
label: kw,
|
|
631
|
+
kind: mk.Keyword,
|
|
632
|
+
insertText: kw,
|
|
633
|
+
range: range
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
suggestions: suggestions
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
var editor = monaco.editor.create(document.getElementById(elementId), {
|
|
643
|
+
value: initialValue || '',
|
|
644
|
+
language: 'logql',
|
|
645
|
+
automaticLayout: true,
|
|
646
|
+
minimap: {
|
|
647
|
+
enabled: false
|
|
648
|
+
},
|
|
649
|
+
lineNumbers: 'off',
|
|
650
|
+
wordWrap: 'on',
|
|
651
|
+
scrollBeyondLastLine: false,
|
|
652
|
+
fontSize: 13,
|
|
653
|
+
overviewRulerLanes: 0,
|
|
654
|
+
folding: false,
|
|
655
|
+
padding: {
|
|
656
|
+
top: 6,
|
|
657
|
+
bottom: 6
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
return {
|
|
661
|
+
getValue: function() {
|
|
662
|
+
return editor.getValue();
|
|
663
|
+
},
|
|
664
|
+
dispose: function() {
|
|
665
|
+
editor.dispose();
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
</script>
|
|
671
|
+
<!-- logger -->
|
|
672
|
+
<style>
|
|
673
|
+
.editorgroupborder {
|
|
674
|
+
border-width: 1px;
|
|
675
|
+
border-style: solid;
|
|
676
|
+
border-color: lightgray;
|
|
677
|
+
padding: 3px;
|
|
678
|
+
margin-top: 2px;
|
|
679
|
+
margin-bottom: 2px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.editorsectionheading {
|
|
683
|
+
font-weight: 600;
|
|
684
|
+
margin-bottom: 1px !important;
|
|
685
|
+
margin-top: 6px !important;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.nomargin {
|
|
689
|
+
margin-bottom: 1px !important;
|
|
690
|
+
margin-top: 1px !important;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.slidecontainer {
|
|
694
|
+
width: 100%;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.slider {
|
|
698
|
+
-webkit-appearance: none;
|
|
699
|
+
appearance: none;
|
|
700
|
+
width: 100%;
|
|
701
|
+
height: 25px;
|
|
702
|
+
background: #d3d3d3;
|
|
703
|
+
outline: none;
|
|
704
|
+
opacity: 0.7;
|
|
705
|
+
-webkit-transition: .2s;
|
|
706
|
+
transition: opacity .2s;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.slider:hover {
|
|
710
|
+
opacity: 1;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.slider::-webkit-slider-thumb {
|
|
714
|
+
-webkit-appearance: none;
|
|
715
|
+
appearance: none;
|
|
716
|
+
width: 25px;
|
|
717
|
+
height: 25px;
|
|
718
|
+
background: #04AA6D;
|
|
719
|
+
cursor: pointer;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.slider::-moz-range-thumb {
|
|
723
|
+
width: 25px;
|
|
724
|
+
height: 25px;
|
|
725
|
+
background: #04AA6D;
|
|
726
|
+
cursor: pointer;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.towb_editorlabel {
|
|
730
|
+
width: 120px !important;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
input.towb_editorfield,
|
|
734
|
+
select.towb_editorfield,
|
|
735
|
+
textarea.towb_editorfield {
|
|
736
|
+
width: 70% !important;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
input.towb_editorfield_short,
|
|
740
|
+
select.towb_editorfield_short {
|
|
741
|
+
width: 30% !important;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
</style>
|
|
745
|
+
<script type="text/javascript">
|
|
746
|
+
// CSS
|
|
747
|
+
dropdownStyles = `
|
|
748
|
+
.loggertype-dropdown {
|
|
749
|
+
position: absolute;
|
|
750
|
+
background: white;
|
|
751
|
+
border: 1px solid #ccc;
|
|
752
|
+
border-radius: 4px;
|
|
753
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
754
|
+
max-height: 150px;
|
|
755
|
+
overflow-y: auto;
|
|
756
|
+
z-index: 1000;
|
|
757
|
+
min-width: 150px;
|
|
758
|
+
}
|
|
759
|
+
.loggertype-dropdown .dropdown-item {
|
|
760
|
+
padding: 6px 12px;
|
|
761
|
+
cursor: pointer;
|
|
762
|
+
}
|
|
763
|
+
.loggertype-dropdown .dropdown-item:hover {
|
|
764
|
+
background: #f0f0f0;
|
|
765
|
+
}
|
|
766
|
+
`;
|
|
767
|
+
$('<style>').text(dropdownStyles).appendTo('head');
|
|
768
|
+
|
|
769
|
+
</script>
|
|
770
|
+
<script type="text/javascript">
|
|
771
|
+
RED.nodes.registerType('LokiQueryNode', {
|
|
772
|
+
category: 'loki',
|
|
773
|
+
icon: 'loki.png',
|
|
774
|
+
color: '#8D7974',
|
|
775
|
+
labelStyle: 'node_label_white',
|
|
776
|
+
label: function() {
|
|
777
|
+
return this.name
|
|
778
|
+
},
|
|
779
|
+
paletteLabel: 'Loki Query',
|
|
780
|
+
inputs: 1,
|
|
781
|
+
inputLabels: (i) => 'input',
|
|
782
|
+
outputs: 1,
|
|
783
|
+
outputLabels: (i) => ['logLines'][i],
|
|
784
|
+
defaults: {
|
|
785
|
+
name: {
|
|
786
|
+
value: 'Loki Query',
|
|
787
|
+
required: true
|
|
788
|
+
},
|
|
789
|
+
lokiservice: {
|
|
790
|
+
type: 'LokiServiceConfigNode',
|
|
791
|
+
required: true
|
|
792
|
+
},
|
|
793
|
+
query: {
|
|
794
|
+
value: '{app="nodered"}',
|
|
795
|
+
required: true
|
|
796
|
+
},
|
|
797
|
+
start: {
|
|
798
|
+
value: '1h',
|
|
799
|
+
required: true
|
|
800
|
+
},
|
|
801
|
+
start_type: {
|
|
802
|
+
value: 'relative',
|
|
803
|
+
required: false
|
|
804
|
+
},
|
|
805
|
+
end: {
|
|
806
|
+
value: '',
|
|
807
|
+
required: false
|
|
808
|
+
},
|
|
809
|
+
end_type: {
|
|
810
|
+
value: 'now',
|
|
811
|
+
required: false
|
|
812
|
+
},
|
|
813
|
+
outputpath: {
|
|
814
|
+
value: 'payload',
|
|
815
|
+
required: false
|
|
816
|
+
},
|
|
817
|
+
batchMode: {
|
|
818
|
+
required: false
|
|
819
|
+
},
|
|
820
|
+
logEnabled: {
|
|
821
|
+
value: false,
|
|
822
|
+
required: true
|
|
823
|
+
},
|
|
824
|
+
logger: {
|
|
825
|
+
required: false,
|
|
826
|
+
value: '',
|
|
827
|
+
type: 'DelegatedConfigReferenceNode'
|
|
828
|
+
},
|
|
829
|
+
logTemplateOverrideEnabled: {
|
|
830
|
+
value: false
|
|
831
|
+
},
|
|
832
|
+
logTemplateOverride: {
|
|
833
|
+
value: 'message:{{msg}}'
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
oneditprepare: function() {
|
|
837
|
+
// **** LokiQueryNode **** //
|
|
838
|
+
{
|
|
839
|
+
let durationRegex = /^\d+(ms|s|m|h|d|w)$/i;
|
|
840
|
+
let timeTypes = [{
|
|
841
|
+
value: "relative",
|
|
842
|
+
label: "Relative",
|
|
843
|
+
icon: "fa fa-clock-o",
|
|
844
|
+
validate: function(v) {
|
|
845
|
+
return durationRegex.test(v);
|
|
846
|
+
},
|
|
847
|
+
hasValue: true
|
|
848
|
+
}, {
|
|
849
|
+
value: "absolute",
|
|
850
|
+
label: "Date/Time",
|
|
851
|
+
icon: "fa fa-calendar",
|
|
852
|
+
validate: function(v) {
|
|
853
|
+
return !isNaN(Date.parse(v));
|
|
854
|
+
},
|
|
855
|
+
hasValue: true
|
|
856
|
+
}];
|
|
857
|
+
$("#node-input-start").typedInput({
|
|
858
|
+
types: timeTypes,
|
|
859
|
+
default: "relative",
|
|
860
|
+
typeField: "#node-input-start_type"
|
|
861
|
+
});
|
|
862
|
+
$("#node-input-end").typedInput({
|
|
863
|
+
types: [{
|
|
864
|
+
value: "now",
|
|
865
|
+
label: "Now",
|
|
866
|
+
icon: "fa fa-circle",
|
|
867
|
+
hasValue: false
|
|
868
|
+
}].concat(timeTypes),
|
|
869
|
+
default: "now",
|
|
870
|
+
typeField: "#node-input-end_type"
|
|
871
|
+
});
|
|
872
|
+
$("#node-input-outputpath").typedInput({
|
|
873
|
+
types: ["msg"]
|
|
874
|
+
});
|
|
875
|
+
$("#node-input-batchMode").prop("checked", this.batchMode === true);
|
|
876
|
+
this.queryEditor = PluginCore.createLogQLEditor("node-input-query-editor", this.query || "");
|
|
877
|
+
}
|
|
878
|
+
// **** logger **** //
|
|
879
|
+
{
|
|
880
|
+
let node = this;
|
|
881
|
+
// controls.
|
|
882
|
+
let logEnabled = $("#node-input-logEnabled");
|
|
883
|
+
let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
|
|
884
|
+
let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
|
|
885
|
+
let logEnabledSection = $("#section_logEnabled");
|
|
886
|
+
// functions.
|
|
887
|
+
let getLoggerTypes = async function() {
|
|
888
|
+
let types = [];
|
|
889
|
+
await $.ajax({
|
|
890
|
+
url: "nodetypeservice/find?tag=LoggerType",
|
|
891
|
+
type: "GET",
|
|
892
|
+
contentType: "application/json; charset=utf-8",
|
|
893
|
+
data: JSON.stringify({
|
|
894
|
+
id: node.id
|
|
895
|
+
}),
|
|
896
|
+
success: (response) => {
|
|
897
|
+
types = response;
|
|
898
|
+
},
|
|
899
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
900
|
+
console.log(jqXHR, textStatus, errorThrown);
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
return types;
|
|
904
|
+
}
|
|
905
|
+
let getLoggers = async () => {
|
|
906
|
+
let loggers = [];
|
|
907
|
+
// get the list of types again.
|
|
908
|
+
let loggerTypes = (await getLoggerTypes()).map(type => type.id);
|
|
909
|
+
// add all of the known loggers.
|
|
910
|
+
RED.nodes.eachConfig(cfg => {
|
|
911
|
+
if (loggerTypes.includes(cfg.type)) {
|
|
912
|
+
loggers.push({
|
|
913
|
+
id: cfg.id,
|
|
914
|
+
name: cfg.label(),
|
|
915
|
+
type: cfg.type
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
return loggers;
|
|
920
|
+
};
|
|
921
|
+
let updateLoggerSelectorList = async () => {
|
|
922
|
+
loggerSelector.empty();
|
|
923
|
+
(await getLoggers()).forEach(logger => {
|
|
924
|
+
$('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
|
|
925
|
+
});
|
|
926
|
+
$('<option value="_ADD_">none</option>').appendTo(loggerSelector);
|
|
927
|
+
}
|
|
928
|
+
logEnabled.on('change', function() {
|
|
929
|
+
if (logEnabled.prop("checked")) {
|
|
930
|
+
logEnabledSection.show();
|
|
931
|
+
node._def.defaults.logger.required = true;
|
|
932
|
+
} else {
|
|
933
|
+
logEnabledSection.hide()
|
|
934
|
+
node._def.defaults.logger.required = false;
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
logTemplateEnabled.on('change', function() {
|
|
938
|
+
if (logTemplateEnabled.prop("checked")) {
|
|
939
|
+
logTemplateEnabledSection.show();
|
|
940
|
+
node._def.defaults.logTemplateOverride.required = true;
|
|
941
|
+
} else {
|
|
942
|
+
logTemplateEnabledSection.hide()
|
|
943
|
+
node._def.defaults.logTemplateOverride.required = false;
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
node.logTemplateOverrideEditor = RED.editor.createEditor({
|
|
947
|
+
id: 'node-input-logTemplateOverrideEditor',
|
|
948
|
+
mode: 'ace/mode/handlebars',
|
|
949
|
+
value: node.logTemplateOverride
|
|
950
|
+
});
|
|
951
|
+
let loggerSelector = $("#logger-selector");
|
|
952
|
+
let loggerEditButton = $("#logger-edit-btn");
|
|
953
|
+
let loggerAddButton = $("#logger-add-btn");
|
|
954
|
+
loggerEditButton.on('click', async function(event) {
|
|
955
|
+
event.stopPropagation();
|
|
956
|
+
if (loggerEditButton.hasClass("disabled")) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
// figure out what type of logger this is.
|
|
960
|
+
let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
|
|
961
|
+
RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
|
|
962
|
+
});
|
|
963
|
+
loggerAddButton.on('click', function(event) {
|
|
964
|
+
event.stopPropagation();
|
|
965
|
+
let $btn = $(event.target);
|
|
966
|
+
// Remove any existing dropdown
|
|
967
|
+
$('.loggertype-dropdown').remove();
|
|
968
|
+
getLoggerTypes().then(loggerTypes => {
|
|
969
|
+
let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
|
|
970
|
+
loggerTypes.forEach(loggerType => {
|
|
971
|
+
$('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
|
|
972
|
+
padding: '4px 8px',
|
|
973
|
+
cursor: 'pointer'
|
|
974
|
+
}).appendTo($dropdown);
|
|
975
|
+
});
|
|
976
|
+
// Append first so outerWidth() is measurable, then position so the
|
|
977
|
+
// right edge of the dropdown aligns with the right edge of the button.
|
|
978
|
+
$('body').append($dropdown);
|
|
979
|
+
$dropdown.css({
|
|
980
|
+
top: $btn.offset().top + $btn.outerHeight(),
|
|
981
|
+
left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
|
|
982
|
+
});
|
|
983
|
+
$(document).on('mousedown.dropdown', function(e) {
|
|
984
|
+
if (!$(e.target).closest('.loggertype-dropdown').length) {
|
|
985
|
+
$dropdown.remove();
|
|
986
|
+
$(document).off('mousedown.dropdown'); // Clean up the listener
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
// Handle item selection
|
|
990
|
+
$dropdown.on('click', '.dropdown-item', function(e) {
|
|
991
|
+
e.stopPropagation();
|
|
992
|
+
const selectedValue = $(this).data('value');
|
|
993
|
+
const selectedText = $(this).text();
|
|
994
|
+
// Do whatever you need with the selection
|
|
995
|
+
console.log('Selected:', selectedValue, selectedText);
|
|
996
|
+
$dropdown.remove();
|
|
997
|
+
RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
|
|
998
|
+
});
|
|
999
|
+
})
|
|
1000
|
+
});
|
|
1001
|
+
loggerSelector.on("focus", updateLoggerSelectorList);
|
|
1002
|
+
loggerSelector.on('change', function(event) {
|
|
1003
|
+
if ("_ADD_" === loggerSelector.val()) {
|
|
1004
|
+
loggerEditButton.addClass("disabled")
|
|
1005
|
+
} else {
|
|
1006
|
+
loggerEditButton.removeClass("disabled")
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
// lastly, make sure that we set the correct value on the logger selector.
|
|
1010
|
+
updateLoggerSelectorList().then(() => {
|
|
1011
|
+
loggerSelector.val(node.logger);
|
|
1012
|
+
loggerSelector.trigger('change');
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
oneditsave: function() {
|
|
1017
|
+
// **** LokiQueryNode **** //
|
|
1018
|
+
{
|
|
1019
|
+
this.query = this.queryEditor.getValue();
|
|
1020
|
+
this.batchMode = $("#node-input-batchMode").is(":checked");
|
|
1021
|
+
this.queryEditor.dispose();
|
|
1022
|
+
delete this.queryEditor;
|
|
1023
|
+
}
|
|
1024
|
+
// **** logger **** //
|
|
1025
|
+
{
|
|
1026
|
+
node = this;
|
|
1027
|
+
var selectedLogger = $("#logger-selector").val();
|
|
1028
|
+
node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
|
|
1029
|
+
node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
|
|
1030
|
+
node.logTemplateOverrideEditor.destroy();
|
|
1031
|
+
delete node.logTemplateOverrideEditor;
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
oneditcancel: function() {
|
|
1035
|
+
// **** LokiQueryNode **** //
|
|
1036
|
+
{
|
|
1037
|
+
if (this.queryEditor) {
|
|
1038
|
+
this.queryEditor.dispose();
|
|
1039
|
+
delete this.queryEditor;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
oneditdelete: function() {},
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
</script>
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
<script type="text/html" data-template-name='LokiQueryNode'>
|
|
1050
|
+
<div id='section_LokiQueryNode'>
|
|
1051
|
+
<div class="form-row">
|
|
1052
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
1053
|
+
<input type="text" id="node-input-name" />
|
|
1054
|
+
</div>
|
|
1055
|
+
|
|
1056
|
+
<div class="form-row">
|
|
1057
|
+
<label for="node-input-lokiservice"><i class="fa fa-server"></i> Loki Service</label>
|
|
1058
|
+
<input type="text" id="node-input-lokiservice" />
|
|
1059
|
+
</div>
|
|
1060
|
+
|
|
1061
|
+
<div class="form-row">
|
|
1062
|
+
<label for="node-input-query-editor"><i class="fa fa-search"></i> Query</label>
|
|
1063
|
+
<div id="node-input-query-editor" style="height:120px; min-height:80px; border:1px solid #ccc; border-radius:3px;"></div>
|
|
1064
|
+
</div>
|
|
1065
|
+
|
|
1066
|
+
<div class="form-row">
|
|
1067
|
+
<label for="node-input-start"><i class="fa fa-clock-o"></i> Start</label>
|
|
1068
|
+
<input type="text" id="node-input-start" />
|
|
1069
|
+
<input type="hidden" id="node-input-start_type" />
|
|
1070
|
+
</div>
|
|
1071
|
+
|
|
1072
|
+
<div class="form-row">
|
|
1073
|
+
<label for="node-input-end"><i class="fa fa-clock-o"></i> End</label>
|
|
1074
|
+
<input type="text" id="node-input-end" />
|
|
1075
|
+
<input type="hidden" id="node-input-end_type" />
|
|
1076
|
+
</div>
|
|
1077
|
+
|
|
1078
|
+
<div class="form-row">
|
|
1079
|
+
<label for="node-input-outputpath"><i class="fa fa-arrow-right"></i> Output Path</label>
|
|
1080
|
+
<input type="text" id="node-input-outputpath" />
|
|
1081
|
+
</div>
|
|
1082
|
+
|
|
1083
|
+
<div class="form-row">
|
|
1084
|
+
<label for="node-input-batchMode"><i class="fa fa-list"></i> Batch Mode</label>
|
|
1085
|
+
<input type="checkbox" id="node-input-batchMode" style="width:auto; margin-top:4px;" />
|
|
1086
|
+
<span style="margin-left:6px; color:#888; font-size:0.9em;">Collect all results into a single array message</span>
|
|
1087
|
+
</div>
|
|
1088
|
+
|
|
1089
|
+
</div>
|
|
1090
|
+
|
|
1091
|
+
<div id='section_logger'>
|
|
1092
|
+
<div id="section_loggerconfig">
|
|
1093
|
+
<div class="form-row editorsectionheading">
|
|
1094
|
+
<i class="w-16 fa fa-eye"></i> <span>Logging</span>
|
|
1095
|
+
</div>
|
|
1096
|
+
<div class="editorgroupborder">
|
|
1097
|
+
<div class="form-row nomargin logEnableCheck">
|
|
1098
|
+
<input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
|
|
1099
|
+
<label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
|
|
1100
|
+
</div>
|
|
1101
|
+
|
|
1102
|
+
<div id="section_logEnabled">
|
|
1103
|
+
<div class="form-row nomargin">
|
|
1104
|
+
<label for="logger-selector">Logger</label>
|
|
1105
|
+
<div style="width: 70%; display: inline-flex;">
|
|
1106
|
+
<select id="logger-selector" style="flex-grow: 1;" class="">
|
|
1107
|
+
<option value="_ADD_">none</option>
|
|
1108
|
+
</select>
|
|
1109
|
+
<a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
|
|
1110
|
+
<i class="fa fa-pencil"></i>
|
|
1111
|
+
</a>
|
|
1112
|
+
<a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
|
|
1113
|
+
<i class="fa fa-plus"></i>
|
|
1114
|
+
</a>
|
|
1115
|
+
</div>
|
|
1116
|
+
</div>
|
|
1117
|
+
|
|
1118
|
+
<div class="form-row nomargin">
|
|
1119
|
+
<input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
|
|
1120
|
+
<label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
|
|
1121
|
+
</div>
|
|
1122
|
+
|
|
1123
|
+
<div id="section_logTemplateOverrideEnabled">
|
|
1124
|
+
<div class="form-row">
|
|
1125
|
+
<label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
|
|
1126
|
+
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
|
|
1133
|
+
</div>
|
|
1134
|
+
</script>
|
|
1135
|
+
|
|
1136
|
+
<script type="text/markdown" data-help-name='LokiQueryNode'>
|
|
1137
|
+
Runs a LogQL query against a Grafana Loki instance and emits log lines as messages.
|
|
1138
|
+
|
|
1139
|
+
### Properties
|
|
1140
|
+
|
|
1141
|
+
: *name* (string) : Display label for this node.
|
|
1142
|
+
: *loki service* (config) : The Loki Service config node that provides the connection URL and credentials.
|
|
1143
|
+
: *query* (LogQL) : The LogQL query to execute. A Monaco editor with LogQL syntax highlighting and completions is provided.
|
|
1144
|
+
: *start* (time) : Start of the query time range. Required. Accepts relative durations or absolute date/times.
|
|
1145
|
+
: *end* (time) : End of the query time range. Defaults to **Now**.
|
|
1146
|
+
: *output path* (msg) : Where in the output message to place each log line object. Defaults to `msg.payload`.
|
|
1147
|
+
: *batch mode* (bool) : When enabled, all results are collected into a single array message. When disabled (default), one message is emitted per log line.
|
|
1148
|
+
|
|
1149
|
+
### Time formats
|
|
1150
|
+
|
|
1151
|
+
The *start* and *end* fields accept:
|
|
1152
|
+
|
|
1153
|
+
- **Relative** - `1h`, `30m`, `7d`, `2w` (subtracted from now at query time)
|
|
1154
|
+
- **Date/Time** - `2024-01-01T00:00:00Z` or any parseable date string
|
|
1155
|
+
|
|
1156
|
+
### Query editor
|
|
1157
|
+
|
|
1158
|
+
The query editor provides:
|
|
1159
|
+
- Syntax highlighting for stream selectors `\{app="nodered"\}`, pipeline stages, metric functions, and aggregation operators.
|
|
1160
|
+
- Snippet completions for all major LogQL constructs (trigger with `Ctrl+Space`).
|
|
1161
|
+
|
|
1162
|
+
### Inputs
|
|
1163
|
+
|
|
1164
|
+
: *msg* : Any message. Used as a trigger.
|
|
1165
|
+
: *msg.query* : *(optional)* Overrides the configured LogQL query.
|
|
1166
|
+
: *msg.start* : *(optional)* Overrides the configured start time (parsed as relative or date string).
|
|
1167
|
+
: *msg.end* : *(optional)* Overrides the configured end time.
|
|
1168
|
+
|
|
1169
|
+
### Outputs
|
|
1170
|
+
|
|
1171
|
+
**Individual mode** (default): one message per log line - a copy of the input message with the output path set to:
|
|
1172
|
+
|
|
1173
|
+
```json
|
|
1174
|
+
\{
|
|
1175
|
+
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
1176
|
+
"line": "the log message text",
|
|
1177
|
+
"labels": \{ "app": "nodered", "level": "error" \}
|
|
1178
|
+
\}
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
**Batch mode**: a single message with the output path set to an array of the above objects.
|
|
1182
|
+
|
|
1183
|
+
### Notes
|
|
1184
|
+
|
|
1185
|
+
- Results are capped at **1 000 log lines** or **10 MB** of response data, whichever is reached first.
|
|
1186
|
+
- Lines are emitted in the order returned by Loki (oldest first within each stream).
|
|
1187
|
+
|
|
1188
|
+
### Logging
|
|
1189
|
+
: *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
|
|
1190
|
+
: *logger* (logconfig) : the endppoint that the node will log events to.
|
|
1191
|
+
: *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
|
|
1192
|
+
: *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
|
|
151
1193
|
</script>
|
|
152
1194
|
|
|
153
1195
|
|