@theotherwillembotha/node-red-loki 0.1.0 → 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,517 +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
- 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
593
  <script type="text/javascript">
771
594
  RED.nodes.registerType('LokiQueryNode', {
772
595
  category: 'loki',
@@ -878,11 +701,14 @@ Mustache/Handlebars templates build the message string pushed to Loki.
878
701
  // **** logger **** //
879
702
  {
880
703
  let node = this;
704
+ let templateConfig = {};
881
705
  // controls.
882
706
  let logEnabled = $("#node-input-logEnabled");
883
707
  let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
884
708
  let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
885
709
  let logEnabledSection = $("#section_logEnabled");
710
+ let loggerSection = $("#section_loggerconfig");
711
+ loggerSection.hide();
886
712
  // functions.
887
713
  let getLoggerTypes = async function() {
888
714
  let types = [];
@@ -1006,10 +832,15 @@ Mustache/Handlebars templates build the message string pushed to Loki.
1006
832
  loggerEditButton.removeClass("disabled")
1007
833
  }
1008
834
  });
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');
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
+ });
1013
844
  });
1014
845
  }
1015
846
  },
@@ -1023,6 +854,8 @@ Mustache/Handlebars templates build the message string pushed to Loki.
1023
854
  }
1024
855
  // **** logger **** //
1025
856
  {
857
+ let node = this;
858
+ let templateConfig = {};
1026
859
  node = this;
1027
860
  var selectedLogger = $("#logger-selector").val();
1028
861
  node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
@@ -1191,5 +1024,4 @@ The query editor provides:
1191
1024
  : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
1192
1025
  : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
1193
1026
  </script>
1194
-
1195
1027