Skip to content

Commit 77b0158

Browse files
fix duplication of level label (#414)
Related issue: #400 Fixed duplictation of level label by deleting the level from original field, cause we have level as separated field. Keep the original level label value if it is different from the calculated value after applying the Log Level Rules for clarity and transparency.
1 parent 002eb1d commit 77b0158

File tree

3 files changed

+345
-2
lines changed

3 files changed

+345
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
* BUGFIX: fix an issue with parsings of the logs lines when in the logs line empty `_stream` and missed `_msg` fields. See [#330](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/330).
66
* BUGFIX: fix applying `Custom query parameters` when querying from Grafana variables. See [#405](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/405).
7+
* BUGFIX: fix duplication of `level` label. Keep the original `level` label as `__orig_level` for clarity and transparency if new calculated label after applying the datasource `Log Level Rules` is different from the original. See [#400](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/400).
78

89
## v0.21.0
910

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import { DataQueryRequest, DataQueryResponse, LogLevel } from "@grafana/data";
2+
3+
import { transformBackendResult } from './backendResultTransformer';
4+
import { LogLevelRule, LogLevelRuleType } from "./configuration/LogLevelRules/types";
5+
import { DerivedFieldConfig } from './types';
6+
7+
describe('transformBackendResult', () => {
8+
const labels = [
9+
{
10+
"_stream_id": "0000000000000000e934a84adb05276890d7f7bfcadabe92",
11+
"custom": "customValue",
12+
"environment": "dev",
13+
"level": "info",
14+
"version": "0.1"
15+
},
16+
{
17+
"_stream_id": "0000000000000000e934a84adb05276890d7f7bfcadabe92",
18+
"custom": "customValue",
19+
"environment": "dev",
20+
"level": "error",
21+
"version": "0.1"
22+
},
23+
{
24+
"_stream_id": "0000000000000000e934a84adb05276890d7f7bfcadabe92",
25+
"custom": "customValue",
26+
"environment": "dev",
27+
"level": "unknown",
28+
"version": "0.1"
29+
},
30+
{
31+
"_stream_id": "0000000000000000e934a84adb05276890d7f7bfcadabe92",
32+
"custom": "customValue",
33+
"environment": "dev",
34+
"level": "information",
35+
"version": "0.1"
36+
},
37+
{
38+
"_stream_id": "0000000000000000e934a84adb05276890d7f7bfcadabe92",
39+
"custom": "customValue",
40+
"environment": "dev",
41+
"level": "debug",
42+
"version": "0.1"
43+
},
44+
{
45+
"_stream_id": "0000000000000000e934a84adb05276890d7f7bfcadabe92",
46+
"cutom": "customValue",
47+
"environment": "dev",
48+
"level": "trace",
49+
"version": "0.1"
50+
},
51+
];
52+
it('should parse level labels and delete origin level labels to avoid duplication', () => {
53+
const response = {
54+
"data": [
55+
{
56+
"refId": "A",
57+
"meta": {
58+
"typeVersion": [
59+
0,
60+
0
61+
]
62+
},
63+
"fields": [
64+
{
65+
"name": "Time",
66+
"type": "time",
67+
"typeInfo": {
68+
"frame": "time.Time"
69+
},
70+
"config": {},
71+
"values": [
72+
1760598702731,
73+
1760598701836,
74+
1760598700825,
75+
1760598697696,
76+
1760598697034,
77+
1760598696367,
78+
],
79+
"entities": {},
80+
"nanos": [
81+
713000,
82+
524000,
83+
282000,
84+
74000,
85+
506000,
86+
620000,
87+
]
88+
},
89+
{
90+
"name": "Line",
91+
"type": "string",
92+
"typeInfo": {
93+
"frame": "string"
94+
},
95+
"config": {},
96+
"values": [
97+
"starting application",
98+
"starting application",
99+
"starting application",
100+
"starting application",
101+
"starting application",
102+
"starting application",
103+
],
104+
"entities": {}
105+
},
106+
{
107+
"name": "labels",
108+
"type": "other",
109+
"typeInfo": {
110+
"frame": "json.RawMessage"
111+
},
112+
"config": {},
113+
"values": labels,
114+
"entities": {}
115+
}
116+
],
117+
"length": 6
118+
}
119+
],
120+
"state": "Done"
121+
} as DataQueryResponse;
122+
const request = {
123+
"app": "dashboard",
124+
"requestId": "SQR100",
125+
"timezone": "browser",
126+
"range": {
127+
"to": "2025-10-16T07:28:02.475Z",
128+
"from": "2025-10-16T01:28:02.475Z",
129+
"raw": {
130+
"from": "now-6h",
131+
"to": "now"
132+
}
133+
},
134+
"interval": "20s",
135+
"intervalMs": 20000,
136+
"targets": [
137+
{
138+
"datasource": {
139+
"type": "victoriametrics-logs-datasource",
140+
"uid": "bexw8wod6s4jke"
141+
},
142+
"editorMode": "code",
143+
"expr": "*",
144+
"queryType": "instant",
145+
"refId": "A",
146+
"maxLines": 1000
147+
}
148+
],
149+
"maxDataPoints": 913,
150+
"scopedVars": {
151+
"__sceneObject": {
152+
"text": "__sceneObject"
153+
},
154+
"__interval": {
155+
"text": "20s",
156+
"value": "20s"
157+
},
158+
"__interval_ms": {
159+
"text": "20000",
160+
"value": 20000
161+
}
162+
},
163+
"startTime": 1760599682628,
164+
"rangeRaw": {
165+
"from": "now-6h",
166+
"to": "now"
167+
},
168+
"dashboardUID": "886b7b9f-97a7-47ee-93b6-9ec7342f6d3e",
169+
"panelId": 1,
170+
"panelName": "New panel",
171+
"panelPluginId": "table",
172+
"dashboardTitle": "double label info"
173+
} as unknown as DataQueryRequest;
174+
const derivedFieldConfigs: DerivedFieldConfig[] = [];
175+
const logLevelRules: LogLevelRule[] = [];
176+
const resultLabels = labels.map(({ level, ...rest }) => rest);
177+
const result = transformBackendResult(response, request, derivedFieldConfigs, logLevelRules);
178+
expect(result.data[0].fields[2].values).toStrictEqual(resultLabels);
179+
expect(result.data[0].fields[3].values).toStrictEqual([
180+
"info",
181+
"error",
182+
"unknown",
183+
"information",
184+
"debug",
185+
"trace",
186+
]);
187+
});
188+
it('should parse level according to rules and left the origin level labels', () => {
189+
const response = {
190+
"data": [
191+
{
192+
"refId": "A",
193+
"meta": {
194+
"typeVersion": [
195+
0,
196+
0
197+
]
198+
},
199+
"fields": [
200+
{
201+
"name": "Time",
202+
"type": "time",
203+
"typeInfo": {
204+
"frame": "time.Time"
205+
},
206+
"config": {},
207+
"values": [
208+
1760598702731,
209+
1760598701836,
210+
1760598700825,
211+
1760598697696,
212+
1760598697034,
213+
1760598696367,
214+
],
215+
"entities": {},
216+
"nanos": [
217+
713000,
218+
524000,
219+
282000,
220+
74000,
221+
506000,
222+
620000,
223+
]
224+
},
225+
{
226+
"name": "Line",
227+
"type": "string",
228+
"typeInfo": {
229+
"frame": "string"
230+
},
231+
"config": {},
232+
"values": [
233+
"starting application",
234+
"starting application",
235+
"starting application",
236+
"starting application",
237+
"starting application",
238+
"starting application",
239+
],
240+
"entities": {}
241+
},
242+
{
243+
"name": "labels",
244+
"type": "other",
245+
"typeInfo": {
246+
"frame": "json.RawMessage"
247+
},
248+
"config": {},
249+
"values": labels,
250+
"entities": {}
251+
}
252+
],
253+
"length": 6
254+
}
255+
],
256+
"state": "Done"
257+
} as DataQueryResponse;
258+
const request = {
259+
"app": "dashboard",
260+
"requestId": "SQR100",
261+
"timezone": "browser",
262+
"range": {
263+
"to": "2025-10-16T07:28:02.475Z",
264+
"from": "2025-10-16T01:28:02.475Z",
265+
"raw": {
266+
"from": "now-6h",
267+
"to": "now"
268+
}
269+
},
270+
"interval": "20s",
271+
"intervalMs": 20000,
272+
"targets": [
273+
{
274+
"datasource": {
275+
"type": "victoriametrics-logs-datasource",
276+
"uid": "bexw8wod6s4jke"
277+
},
278+
"editorMode": "code",
279+
"expr": "*",
280+
"queryType": "instant",
281+
"refId": "A",
282+
"maxLines": 1000
283+
}
284+
],
285+
"maxDataPoints": 913,
286+
"scopedVars": {
287+
"__sceneObject": {
288+
"text": "__sceneObject"
289+
},
290+
"__interval": {
291+
"text": "20s",
292+
"value": "20s"
293+
},
294+
"__interval_ms": {
295+
"text": "20000",
296+
"value": 20000
297+
}
298+
},
299+
"startTime": 1760599682628,
300+
"rangeRaw": {
301+
"from": "now-6h",
302+
"to": "now"
303+
},
304+
"dashboardUID": "886b7b9f-97a7-47ee-93b6-9ec7342f6d3e",
305+
"panelId": 1,
306+
"panelName": "New panel",
307+
"panelPluginId": "table",
308+
"dashboardTitle": "double label info"
309+
} as unknown as DataQueryRequest;
310+
const derivedFieldConfigs: DerivedFieldConfig[] = [];
311+
const logLevelRules: LogLevelRule[] = [{
312+
enabled: true,
313+
field: 'environment',
314+
operator: LogLevelRuleType.Equals,
315+
value: 'dev',
316+
level: LogLevel.critical
317+
}];
318+
const resultLabels = labels.map(({ level, ...rest }) => ({
319+
...rest,
320+
__orig_level: level,
321+
}));
322+
const result = transformBackendResult(response, request, derivedFieldConfigs, logLevelRules);
323+
expect(result.data[0].fields[2].values).toStrictEqual(resultLabels);
324+
expect(result.data[0].fields[3].values).toStrictEqual([
325+
LogLevel.critical,
326+
LogLevel.critical,
327+
LogLevel.critical,
328+
LogLevel.critical,
329+
LogLevel.critical,
330+
LogLevel.critical,
331+
]);
332+
});
333+
});

src/backendResultTransformer.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
DataQueryResponse,
66
Field,
77
FieldType,
8+
isDataFrame,
89
LogLevel,
910
QueryResultMeta,
10-
isDataFrame,
1111
} from '@grafana/data';
1212

1313
import { LogLevelRule } from "./configuration/LogLevelRules/types";
@@ -46,7 +46,16 @@ function addLevelField(frame: DataFrame, rules: LogLevelRule[]): DataFrame {
4646

4747
const levelValues: LogLevel[] = Array.from({ length: rows }, (_, idx) => {
4848
const labels = (labelsField?.values[idx] ?? {}) as Record<string, any>;
49-
return extractLevelFromLabels(labels, rules)
49+
const level = extractLevelFromLabels(labels, rules);
50+
51+
// save the original level if it's different from the extracted one to show original values
52+
if (level !== labels.level) {
53+
labels.__orig_level = labels.level;
54+
}
55+
56+
// delete level label to avoid duplication, level is now a separate field
57+
delete labels.level;
58+
return level;
5059
});
5160

5261
const levelField: Field<LogLevel> = {

0 commit comments

Comments
 (0)