@theotherwillembotha/node-red-loki 0.0.56 → 0.3.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/build/Nodes.html CHANGED
@@ -7,7 +7,339 @@
7
7
  You have been warned.
8
8
 
9
9
  -->
10
+ <!-- LokiQueryNode -->
11
+
12
+ <script type="text/javascript">
13
+
14
+ window.PluginCore = window.PluginCore || {};
15
+
16
+ /**
17
+ * PluginCore.createLogQLEditor(elementId, initialValue)
18
+ *
19
+ * Creates a Monaco editor pre-configured with the LogQL language:
20
+ * syntax highlighting for stream selectors, pipeline stages, metric
21
+ * functions, and aggregation operators; plus snippet completions.
22
+ *
23
+ * The LogQL language is registered with Monaco lazily on first call.
24
+ *
25
+ * @param {string} elementId - DOM id of the container element.
26
+ * @param {string} initialValue - Starting content of the editor.
27
+ * @returns {{ getValue(): string, dispose(): void \}}
28
+ */
29
+ PluginCore.createLogQLEditor = function(elementId, initialValue) {
30
+
31
+ // Register the LogQL language with Monaco once per session.
32
+ if (!PluginCore._logqlReady) {
33
+ PluginCore._logqlReady = true;
34
+
35
+ monaco.languages.register({ id: 'logql' });
36
+
37
+ monaco.languages.setLanguageConfiguration('logql', {
38
+ brackets: [['{', '}'], ['[', ']'], ['(', ')']],
39
+ autoClosingPairs: [
40
+ { open: '{', close: '}' },
41
+ { open: '[', close: ']' },
42
+ { open: '(', close: ')' },
43
+ { open: '"', close: '"' },
44
+ { open: "'", close: "'" },
45
+ { open: '`', close: '`' }
46
+ ],
47
+ surroundingPairs: [
48
+ { open: '{', close: '}' },
49
+ { open: '[', close: ']' },
50
+ { open: '(', close: ')' },
51
+ { open: '"', close: '"' },
52
+ { open: "'", close: "'" }
53
+ ]
54
+ });
55
+
56
+ monaco.languages.setMonarchTokensProvider('logql', {
57
+ defaultToken: '',
58
+ tokenPostfix: '.logql',
59
+
60
+ keywords: ['by', 'without', 'on', 'ignoring', 'group_left', 'group_right', 'bool', 'or', 'and', 'unless', 'offset'],
61
+ metricFns: ['rate', 'count_over_time', 'bytes_rate', 'bytes_over_time', 'absent_over_time',
62
+ 'sum_over_time', 'avg_over_time', 'min_over_time', 'max_over_time',
63
+ 'first_over_time', 'last_over_time', 'stddev_over_time', 'stdvar_over_time',
64
+ 'quantile_over_time', 'label_replace', 'vector', 'sort', 'sort_desc'],
65
+ aggregations: ['sum', 'avg', 'min', 'max', 'count', 'stddev', 'stdvar', 'bottomk', 'topk'],
66
+ pipelineStages: ['json', 'logfmt', 'pattern', 'regexp', 'unpack', 'line_format',
67
+ 'label_format', 'drop', 'keep', 'decolorize', 'unwrap', 'ip',
68
+ 'duration', 'bytes', 'distinct'],
69
+
70
+ tokenizer: {
71
+ root: [
72
+ // Stream selector open brace
73
+ [/\{/, { token: 'delimiter.curly', bracket: '@open', next: '@streamSelector' }],
74
+ // Range vector e.g. [5m]
75
+ [/\[\d+(ms|s|m|h|d|w|y)\]/, 'number.duration'],
76
+ // Pipeline filter operators
77
+ [/[|!][=~]/, 'operator.filter'],
78
+ // Pipe
79
+ [/\|/, 'operator.pipe'],
80
+ // Comparison operators
81
+ [/[><=!]+/, 'operator'],
82
+ // Numbers
83
+ [/\d+(\.\d+)?/, 'number'],
84
+ // Identifiers -> map to keyword/function/aggregation/pipeline categories
85
+ [/[a-zA-Z_]\w*/, {
86
+ cases: {
87
+ '@keywords': 'keyword',
88
+ '@aggregations': 'type.aggregate',
89
+ '@metricFns': 'type.function',
90
+ '@pipelineStages': 'type.pipeline',
91
+ '@default': 'identifier'
92
+ }
93
+ }],
94
+ // String literals
95
+ [/"/, { token: 'string.quote', bracket: '@open', next: '@dblString' }],
96
+ [/'/, { token: 'string.quote', bracket: '@open', next: '@sglString' }],
97
+ [/`/, { token: 'string.quote', bracket: '@open', next: '@rawString' }]
98
+ ],
99
+
100
+ streamSelector: [
101
+ [/\}/, { token: 'delimiter.curly', bracket: '@close', next: '@pop' }],
102
+ [/[a-zA-Z_]\w*/, 'attribute.name'],
103
+ [/=~|!=|=|!~/, 'operator'],
104
+ [/"/, { token: 'string.quote', bracket: '@open', next: '@dblString' }],
105
+ [/'/, { token: 'string.quote', bracket: '@open', next: '@sglString' }],
106
+ [/`/, { token: 'string.quote', bracket: '@open', next: '@rawString' }],
107
+ [/,/, 'delimiter']
108
+ ],
109
+
110
+ dblString: [
111
+ [/[^"\\]+/, 'string'],
112
+ [/\\./, 'string.escape'],
113
+ [/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
114
+ ],
115
+ sglString: [
116
+ [/[^'\\]+/, 'string'],
117
+ [/\\./, 'string.escape'],
118
+ [/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
119
+ ],
120
+ rawString: [
121
+ [/[^`]+/, 'string'],
122
+ [/`/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
123
+ ]
124
+ }
125
+ });
126
+
127
+ monaco.languages.registerCompletionItemProvider('logql', {
128
+ triggerCharacters: ['{', '|', ' ', '('],
129
+ provideCompletionItems: function(model, position) {
130
+ var word = model.getWordUntilPosition(position);
131
+ var range = {
132
+ startLineNumber: position.lineNumber,
133
+ endLineNumber: position.lineNumber,
134
+ startColumn: word.startColumn,
135
+ endColumn: word.endColumn
136
+ };
137
+
138
+ var mk = monaco.languages.CompletionItemKind;
139
+ var SR = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
140
+ var suggestions = [];
141
+
142
+ // Stream selector template
143
+ suggestions.push({
144
+ label: '{label="value"}',
145
+ kind: mk.Snippet,
146
+ detail: 'Log stream selector',
147
+ documentation: 'Select log streams matching a set of label matchers.',
148
+ insertText: '{${1:app}="${2:nodered}"}',
149
+ insertTextRules: SR,
150
+ range: range
151
+ });
152
+
153
+ // Metric query functions
154
+ [
155
+ { l: 'rate', d: 'Per-second rate of log entries', s: 'rate({${1:app}="${2}"} [${3:5m}])' },
156
+ { l: 'count_over_time', d: 'Count of log entries over time range', s: 'count_over_time({${1:app}="${2}"} [${3:5m}])' },
157
+ { l: 'bytes_rate', d: 'Per-second byte rate of log entries', s: 'bytes_rate({${1:app}="${2}"} [${3:5m}])' },
158
+ { l: 'bytes_over_time', d: 'Total bytes of log entries over range', s: 'bytes_over_time({${1:app}="${2}"} [${3:5m}])' },
159
+ { l: 'absent_over_time', d: 'Returns 1 if no entries found in range', s: 'absent_over_time({${1:app}="${2}"} [${3:5m}])' },
160
+ { l: 'sum_over_time', d: 'Sum of unwrapped values over range', s: 'sum_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])' },
161
+ { l: 'avg_over_time', d: 'Average of unwrapped values over range', s: 'avg_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])' },
162
+ { l: 'min_over_time', d: 'Minimum of unwrapped values over range', s: 'min_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])' },
163
+ { l: 'max_over_time', d: 'Maximum of unwrapped values over range', s: 'max_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])' },
164
+ { l: 'quantile_over_time', d: 'Quantile of unwrapped values', s: 'quantile_over_time(${1:0.99}, {${2:app}="${3}"} | unwrap ${4:field} [${5:5m}])' },
165
+ { l: 'first_over_time', d: 'First unwrapped value over range', s: 'first_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])' },
166
+ { l: 'last_over_time', d: 'Last unwrapped value over range', s: 'last_over_time({${1:app}="${2}"} | unwrap ${3:field} [${4:5m}])' },
167
+ { l: 'label_replace', d: 'Replace label value using regex', s: 'label_replace(${1:expr}, "${2:dst_label}", "${3:$1}", "${4:src_label}", "${5:(.*)}")' }
168
+ ].forEach(function(f) {
169
+ suggestions.push({ label: f.l, kind: mk.Function, detail: f.d, insertText: f.s, insertTextRules: SR, range: range });
170
+ });
171
+
172
+ // Aggregation operators
173
+ [
174
+ { l: 'sum', d: 'Sum over dimensions' },
175
+ { l: 'avg', d: 'Average over dimensions' },
176
+ { l: 'min', d: 'Minimum over dimensions' },
177
+ { l: 'max', d: 'Maximum over dimensions' },
178
+ { l: 'count', d: 'Count over dimensions' },
179
+ { l: 'stddev', d: 'Standard deviation' },
180
+ { l: 'stdvar', d: 'Standard variance' },
181
+ { l: 'bottomk', d: 'Smallest k elements', s: 'bottomk(${1:5}, ${2:expr})' },
182
+ { l: 'topk', d: 'Largest k elements', s: 'topk(${1:5}, ${2:expr})' }
183
+ ].forEach(function(a) {
184
+ var snippet = a.s || (a.l + '(${1:by (${2:label})}) (${3:expr})');
185
+ suggestions.push({ label: a.l, kind: mk.Keyword, detail: a.d, insertText: snippet, insertTextRules: SR, range: range });
186
+ });
187
+
188
+ // Pipeline stages
189
+ [
190
+ { l: '|= "..."', s: '|= "${1}"', d: 'Filter: line contains string' },
191
+ { l: '!= "..."', s: '!= "${1}"', d: 'Filter: line does not contain string' },
192
+ { l: '|~ "..."', s: '|~ "${1}"', d: 'Filter: line matches regex' },
193
+ { l: '!~ "..."', s: '!~ "${1}"', d: 'Filter: line does not match regex' },
194
+ { l: '| json', s: '| json', d: 'Parse JSON log line' },
195
+ { l: '| logfmt', s: '| logfmt', d: 'Parse logfmt log line' },
196
+ { l: '| pattern "..."', s: '| pattern "${1:<_> level=<level> <_>}"', d: 'Parse using pattern' },
197
+ { l: '| regexp "..."', s: '| regexp "${1:(?P<level>[a-z]+)}"', d: 'Parse using named regex groups' },
198
+ { l: '| line_format "..."', s: '| line_format "${1:{{.level\}} {{.msg\}}}"', d: 'Reformat the log line' },
199
+ { l: '| label_format', s: '| label_format ${1:new_label}=${2:old_label}', d: 'Rename or transform labels' },
200
+ { l: '| unwrap', s: '| unwrap ${1:label}', d: 'Extract numeric value for metric queries' },
201
+ { l: '| unpack', s: '| unpack', d: 'Unpack JSON log fields into labels' },
202
+ { l: '| drop', s: '| drop ${1:label}', d: 'Drop specified labels' },
203
+ { l: '| keep', s: '| keep ${1:label}', d: 'Keep only specified labels' },
204
+ { l: '| decolorize', s: '| decolorize', d: 'Strip ANSI colour codes' }
205
+ ].forEach(function(p) {
206
+ suggestions.push({ label: p.l, kind: mk.Operator, detail: p.d, insertText: p.s, insertTextRules: SR, range: range });
207
+ });
208
+
209
+ // Keywords
210
+ ['by', 'without', 'on', 'or', 'and', 'unless', 'bool', 'offset', 'ignoring'].forEach(function(kw) {
211
+ suggestions.push({ label: kw, kind: mk.Keyword, insertText: kw, range: range });
212
+ });
213
+
214
+ return { suggestions: suggestions };
215
+ }
216
+ });
217
+ }
218
+
219
+ var editor = monaco.editor.create(document.getElementById(elementId), {
220
+ value: initialValue || '',
221
+ language: 'logql',
222
+ automaticLayout: true,
223
+ minimap: { enabled: false },
224
+ lineNumbers: 'off',
225
+ wordWrap: 'on',
226
+ scrollBeyondLastLine: false,
227
+ fontSize: 13,
228
+ overviewRulerLanes: 0,
229
+ folding: false,
230
+ padding: { top: 6, bottom: 6 }
231
+ });
232
+
233
+ return {
234
+ getValue: function() { return editor.getValue(); },
235
+ dispose: function() { editor.dispose(); }
236
+ };
237
+ };
238
+
239
+ </script>
240
+
241
+
242
+ <!-- logger -->
243
+
244
+ <style>
245
+ .editorgroupborder {
246
+ border-width:1px;
247
+ border-style:solid;
248
+ border-color:lightgray;
249
+ padding: 3px;
250
+ margin-top: 2px;
251
+ margin-bottom: 2px;
252
+ }
253
+
254
+ .editorsectionheading {
255
+ font-weight: 600;
256
+ margin-bottom:1px !important;
257
+ margin-top:6px !important;
258
+ }
259
+
260
+ .nomargin {
261
+ margin-bottom:1px !important;
262
+ margin-top:1px !important;
263
+ }
264
+
265
+ .slidecontainer {
266
+ width: 100%;
267
+ }
268
+
269
+ .slider {
270
+ -webkit-appearance: none;
271
+ appearance: none;
272
+ width: 100%;
273
+ height: 25px;
274
+ background: #d3d3d3;
275
+ outline: none;
276
+ opacity: 0.7;
277
+ -webkit-transition: .2s;
278
+ transition: opacity .2s;
279
+ }
280
+
281
+ .slider:hover {
282
+ opacity: 1;
283
+ }
284
+
285
+ .slider::-webkit-slider-thumb {
286
+ -webkit-appearance: none;
287
+ appearance: none;
288
+ width: 25px;
289
+ height: 25px;
290
+ background: #04AA6D;
291
+ cursor: pointer;
292
+ }
293
+
294
+ .slider::-moz-range-thumb {
295
+ width: 25px;
296
+ height: 25px;
297
+ background: #04AA6D;
298
+ cursor: pointer;
299
+ }
10
300
 
301
+ .towb_editorlabel {
302
+ width: 120px !important;
303
+ }
304
+ input.towb_editorfield, select.towb_editorfield, textarea.towb_editorfield {
305
+ width: 70% !important;
306
+ }
307
+ input.towb_editorfield_short, select.towb_editorfield_short {
308
+ width: 30% !important;
309
+ }
310
+
311
+ </style>
312
+
313
+ <script type="text/javascript">
314
+
315
+ // CSS
316
+ dropdownStyles = `
317
+ .loggertype-dropdown {
318
+ position: absolute;
319
+ background: white;
320
+ border: 1px solid #ccc;
321
+ border-radius: 4px;
322
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
323
+ max-height: 150px;
324
+ overflow-y: auto;
325
+ z-index: 1000;
326
+ min-width: 150px;
327
+ }
328
+ .loggertype-dropdown .dropdown-item {
329
+ padding: 6px 12px;
330
+ cursor: pointer;
331
+ }
332
+ .loggertype-dropdown .dropdown-item:hover {
333
+ background: #f0f0f0;
334
+ }
335
+ `;
336
+ $('<style>').text(dropdownStyles).appendTo('head');
337
+
338
+ </script>
339
+
340
+
341
+
342
+
11
343
  <script type="text/javascript">
12
344
  RED.nodes.registerType('LokiServiceConfigNode', {
13
345
  category: 'config',
@@ -130,6 +462,7 @@ Shared connection configuration for a Grafana Loki instance. Referenced by the L
130
462
  : *username* (string) : Username for basic authentication. Leave blank if auth is not required.
131
463
  : *auth token* (string) : Password or token for basic authentication.
132
464
  </script>
465
+
133
466
  <script type="text/javascript">
134
467
  RED.nodes.registerType('LokiLoggerConfigNode', {
135
468
  category: 'config',
@@ -256,514 +589,7 @@ Mustache/Handlebars templates build the message string pushed to Loki.
256
589
  - [Mustache - Wikipedia](https://en.wikipedia.org/wiki/Mustache_(template_system))
257
590
  - [Mustache - Playground](https://jgonggrijp.gitlab.io/wontache/playground.html)
258
591
  </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
592
 
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
- .towb_editorfield {
734
- width: 70% !important;
735
- }
736
-
737
- .towb_editorfield_short {
738
- width: 30% !important;
739
- }
740
-
741
- </style>
742
- <script type="text/javascript">
743
- // CSS
744
- dropdownStyles = `
745
- .loggertype-dropdown {
746
- position: absolute;
747
- background: white;
748
- border: 1px solid #ccc;
749
- border-radius: 4px;
750
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
751
- max-height: 150px;
752
- overflow-y: auto;
753
- z-index: 1000;
754
- min-width: 150px;
755
- }
756
- .loggertype-dropdown .dropdown-item {
757
- padding: 6px 12px;
758
- cursor: pointer;
759
- }
760
- .loggertype-dropdown .dropdown-item:hover {
761
- background: #f0f0f0;
762
- }
763
- `;
764
- $('<style>').text(dropdownStyles).appendTo('head');
765
-
766
- </script>
767
593
  <script type="text/javascript">
768
594
  RED.nodes.registerType('LokiQueryNode', {
769
595
  category: 'loki',
@@ -875,11 +701,14 @@ Mustache/Handlebars templates build the message string pushed to Loki.
875
701
  // **** logger **** //
876
702
  {
877
703
  let node = this;
704
+ let templateConfig = {};
878
705
  // controls.
879
706
  let logEnabled = $("#node-input-logEnabled");
880
707
  let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
881
708
  let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
882
709
  let logEnabledSection = $("#section_logEnabled");
710
+ let loggerSection = $("#section_loggerconfig");
711
+ loggerSection.hide();
883
712
  // functions.
884
713
  let getLoggerTypes = async function() {
885
714
  let types = [];
@@ -1003,10 +832,15 @@ Mustache/Handlebars templates build the message string pushed to Loki.
1003
832
  loggerEditButton.removeClass("disabled")
1004
833
  }
1005
834
  });
1006
- // lastly, make sure that we set the correct value on the logger selector.
1007
- updateLoggerSelectorList().then(() => {
1008
- loggerSelector.val(node.logger);
1009
- loggerSelector.trigger('change');
835
+ // Show the logger section only if at least one logger type is registered.
836
+ // If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
837
+ getLoggerTypes().then(loggerTypes => {
838
+ if (loggerTypes.length === 0) return;
839
+ loggerSection.show();
840
+ updateLoggerSelectorList().then(() => {
841
+ loggerSelector.val(node.logger);
842
+ loggerSelector.trigger('change');
843
+ });
1010
844
  });
1011
845
  }
1012
846
  },
@@ -1020,6 +854,8 @@ Mustache/Handlebars templates build the message string pushed to Loki.
1020
854
  }
1021
855
  // **** logger **** //
1022
856
  {
857
+ let node = this;
858
+ let templateConfig = {};
1023
859
  node = this;
1024
860
  var selectedLogger = $("#logger-selector").val();
1025
861
  node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
@@ -1188,5 +1024,4 @@ The query editor provides:
1188
1024
  : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
1189
1025
  : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
1190
1026
  </script>
1191
-
1192
1027