Skip to content

Commit 41409ac

Browse files
authored
feat: updated full time range logs volume histogram (#5)
* fix: added logs model for building logs volume graph * fix: By default added fields to data frame * fix: update logs volume dataframe * fix: updated folder name * fix: removed console logs * fix: optimised search api calls
1 parent 3416082 commit 41409ac

File tree

12 files changed

+1656
-279
lines changed

12 files changed

+1656
-279
lines changed

.DS_Store

2 KB
Binary file not shown.

package-lock.json

Lines changed: 1093 additions & 131 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,16 @@
5252
"typescript": "^4.4.0",
5353
"webpack": "^5.69.1",
5454
"webpack-cli": "^4.9.2",
55-
"webpack-livereload-plugin": "^3.0.2"
55+
"webpack-livereload-plugin": "^3.0.2",
56+
"rxjs": "7.8.0"
5657
},
5758
"engines": {
5859
"node": ">=16"
5960
},
6061
"dependencies": {
6162
"@emotion/css": "^11.1.3",
62-
"@grafana/data": "9.3.8",
63-
"@grafana/runtime": "9.3.8",
63+
"@grafana/data": "9.5.2",
64+
"@grafana/runtime": "9.5.2",
6465
"@grafana/ui": "9.3.8",
6566
"monaco-editor": "^0.34.0",
6667
"react": "17.0.2",

src/.DS_Store

2 KB
Binary file not shown.

src/components/QueryEditor.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const QueryEditor = ({ query, onChange, onRunQuery, datasource }: Props)
1515
const [streamDetails, setStreamDetails]: any = useState({});
1616
const [streamOptions, setStreamOptions]: any = useState([]);
1717
const [orgOptions, setOrgOptions]: any = useState([]);
18+
const [isMounted, setIsMounted]: any = useState(false);
1819

1920
useEffect(() => {
2021
getOrganizations({ url: datasource.url, page_num: 0, page_size: 1000, sort_by: 'id' })
@@ -26,36 +27,38 @@ export const QueryEditor = ({ query, onChange, onRunQuery, datasource }: Props)
2627
})),
2728
]);
2829
setupStreams(orgs.data[0].name).then((streams: any) => {
29-
onChange({
30-
...query,
31-
stream: streams[0].name,
32-
organization: orgs.data[0].name,
33-
sqlMode: false,
34-
});
3530
datasource.updateStreamFields(streams[0].schema);
3631
setStreamOptions([
3732
...Object.values(streams).map((stream: any) => ({
3833
label: stream.name,
3934
value: stream.name,
4035
})),
4136
]);
42-
onRunQuery();
37+
if (!(query.organization && query.stream && query.hasOwnProperty('sqlMode'))) {
38+
onChange({
39+
...query,
40+
stream: streams[0].name,
41+
organization: orgs.data[0].name,
42+
sqlMode: false,
43+
});
44+
}
45+
setIsMounted(true);
4346
});
4447
})
4548
.catch((err) => console.log(err));
4649
// eslint-disable-next-line react-hooks/exhaustive-deps
4750
}, []);
4851

4952
useEffect(() => {
50-
if (query.sqlMode !== undefined && query.stream) {
53+
if (query.sqlMode !== undefined && query.stream && isMounted) {
5154
updateQuery();
5255
}
5356

5457
// eslint-disable-next-line react-hooks/exhaustive-deps
5558
}, [query.sqlMode, query.organization, query.stream]);
5659

5760
useEffect(() => {
58-
if (query.stream && query.organization) {
61+
if (query.stream && query.organization && isMounted) {
5962
updateQuery();
6063
onRunQuery();
6164
}
@@ -117,7 +120,7 @@ export const QueryEditor = ({ query, onChange, onRunQuery, datasource }: Props)
117120
stream: streams[0].name,
118121
organization: organization.value,
119122
});
120-
datasource.updateStreamFields(cloneDeep(streamDetails[streams[0].name].schema));
123+
datasource.updateStreamFields(cloneDeep(streams[0].schema));
121124
setStreamOptions([
122125
...streams.map((stream: any) => ({
123126
label: stream.name,

src/datasource.test.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MyDataSourceOptions } from 'types';
22
import { DataSource } from './datasource';
33
import { DataSourceInstanceSettings, PluginSignatureStatus, PluginType } from '@grafana/data';
4+
import { buildQuery } from 'features/query/queryBuilder';
45

56
let DateTime = {
67
add: jest.fn(),
@@ -28,6 +29,12 @@ let DateTime = {
2829
minute: jest.fn(),
2930
};
3031

32+
jest.mock('rxjs', () => {
33+
return {
34+
Observable: jest.fn(),
35+
};
36+
});
37+
3138
jest.mock('@grafana/runtime', () => ({
3239
...jest.requireActual('@grafana/runtime'),
3340
getBackendSrv: () => {
@@ -120,21 +127,6 @@ describe('DataSource', () => {
120127
ds = new DataSource(instanceSettings);
121128
});
122129

123-
describe('getConsumableTime', () => {
124-
it('should convert range to microsecond format', () => {
125-
const range = {
126-
from: new Date('2023-05-16T00:00:00Z'),
127-
to: new Date('2023-05-16T01:00:00Z'),
128-
};
129-
const result = ds.getConsumableTime(range);
130-
131-
expect(result).toEqual({
132-
startTimeInMicro: range.from.getTime() * 1000,
133-
endTimeInMirco: range.to.getTime() * 1000,
134-
});
135-
});
136-
});
137-
138130
describe('testDatasource', () => {
139131
it('should return success status', async () => {
140132
const result = await ds.testDatasource();
@@ -405,7 +397,7 @@ describe('DataSource', () => {
405397
encoding: 'base64',
406398
};
407399
beforeEach(async () => {
408-
result = ds.buildQuery(queryData, timestamps);
400+
result = buildQuery(queryData, timestamps, queryData.streamFields);
409401
});
410402
it('should return query request data', () => {
411403
expect(JSON.stringify(result)).toMatch(JSON.stringify(expectedReq));

src/datasource.ts

Lines changed: 78 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,28 @@ import {
33
DataQueryResponse,
44
DataSourceApi,
55
DataSourceInstanceSettings,
6-
MutableDataFrame,
7-
FieldType,
86
QueryFixAction,
7+
DataSourceWithSupplementaryQueriesSupport,
8+
SupplementaryQueryType,
9+
LogLevel,
910
} from '@grafana/data';
10-
11+
import { Observable } from 'rxjs';
1112
import { getBackendSrv } from '@grafana/runtime';
13+
import { queryLogsVolume } from './features/log/LogsModel';
1214

13-
import { MyQuery, MyDataSourceOptions, TimeRange } from './types';
14-
import { b64EncodeUnicode, logsErrorMessage } from 'utils/zincutils';
15+
import { MyQuery, MyDataSourceOptions } from './types';
16+
import { logsErrorMessage, getConsumableTime } from 'utils/zincutils';
1517
import { getOrganizations } from 'services/organizations';
16-
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
18+
import { cloneDeep } from 'lodash';
19+
import { getGraphDataFrame, getLogsDataFrame } from 'features/log/queryResponseBuilder';
20+
import { buildQuery } from './features/query/queryBuilder';
21+
22+
const REF_ID_STARTER_LOG_VOLUME = 'log-volume-';
23+
24+
export class DataSource
25+
extends DataSourceApi<MyQuery, MyDataSourceOptions>
26+
implements DataSourceWithSupplementaryQueriesSupport<MyQuery>
27+
{
1728
instanceSettings?: DataSourceInstanceSettings<MyDataSourceOptions>;
1829
url: string;
1930
streamFields: any[];
@@ -25,70 +36,34 @@ export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
2536
this.streamFields = [];
2637
}
2738

28-
getConsumableTime(range: any) {
29-
const startTimeInMicro: any = new Date(new Date(range!.from.valueOf()).toISOString()).getTime() * 1000;
30-
const endTimeInMirco: any = new Date(new Date(range!.to.valueOf()).toISOString()).getTime() * 1000;
31-
return {
32-
startTimeInMicro,
33-
endTimeInMirco,
34-
};
35-
}
36-
37-
buildLogsDataFrame(logs: any[], target: MyQuery) {
38-
const fieldsMapping: { [key: string]: FieldType } = {
39-
Utf8: FieldType.string,
40-
Int64: FieldType.number,
41-
timestamp: FieldType.time,
42-
};
43-
44-
const fields = [
45-
{ name: 'Time', type: FieldType.time },
46-
{ name: 'Content', type: FieldType.string },
47-
];
48-
49-
this.streamFields.forEach((field: any) => {
50-
fields.push({
51-
name: field.name,
52-
type: fieldsMapping[field.type],
53-
});
54-
});
55-
56-
const frame = new MutableDataFrame({
57-
refId: target.refId,
58-
meta: {
59-
preferredVisualisationType: 'logs',
60-
},
61-
fields,
62-
});
63-
64-
logs.forEach((log: any) => {
65-
frame.add({ ...log, Content: JSON.stringify(log) });
66-
});
67-
68-
return frame;
69-
}
70-
7139
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
72-
const timestamps = this.getConsumableTime(options.range);
40+
const timestamps = getConsumableTime(options.range);
7341
const promises = options.targets.map((target) => {
74-
const reqData = this.buildQuery(target, timestamps);
42+
const reqData = buildQuery(target, timestamps, this.streamFields);
7543
return this.doRequest(target, reqData)
7644
.then((response) => {
77-
return this.buildLogsDataFrame(response.hits, target);
45+
if (target?.refId?.includes(REF_ID_STARTER_LOG_VOLUME)) {
46+
return getGraphDataFrame(response, target);
47+
}
48+
return getLogsDataFrame(response, target, this.streamFields);
7849
})
7950
.catch((err) => {
80-
let error = '';
81-
if (err.response !== undefined) {
82-
error = err.response.data.error;
51+
let error = {
52+
message: '',
53+
detail: '',
54+
};
55+
if (err.data) {
56+
error.message = err.data?.message;
57+
error.detail = err.data?.error_detail;
8358
} else {
84-
error = err.message;
59+
error.message = err.statusText;
8560
}
8661

87-
const customMessage = logsErrorMessage(err.response.data.code);
62+
const customMessage = logsErrorMessage(err.data.code);
8863
if (customMessage) {
89-
error = customMessage;
64+
error.message = customMessage;
9065
}
91-
throw new Error(error);
66+
throw new Error(error.message + (error.detail ? ` ( ${error.detail} ) ` : ''));
9267
});
9368
});
9469

@@ -146,68 +121,54 @@ export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
146121
return { ...query, query: expression };
147122
}
148123

149-
buildQuery(queryData: MyQuery, timestamps: TimeRange) {
150-
try {
151-
let query: string = queryData.query || '';
152-
153-
let req: any = {
154-
query: {
155-
sql: 'select * from "[INDEX_NAME]" [WHERE_CLAUSE]',
156-
start_time: timestamps.startTimeInMicro,
157-
end_time: timestamps.endTimeInMirco,
158-
size: 150,
159-
sql_mode: 'full',
160-
},
161-
};
162-
163-
if (queryData.sqlMode) {
164-
req.query.sql = queryData.query;
165-
}
124+
updateStreamFields(streamFields: any[]) {
125+
this.streamFields = [...streamFields];
126+
}
166127

167-
if (!queryData.sqlMode) {
168-
let whereClause = query;
169-
170-
if (query.trim().length) {
171-
whereClause = whereClause
172-
.replace(/=(?=(?:[^"']*"[^"']*"')*[^"']*$)/g, ' =')
173-
.replace(/>(?=(?:[^"']*"[^"']*"')*[^"']*$)/g, ' >')
174-
.replace(/<(?=(?:[^"']*"[^"']*"')*[^"']*$)/g, ' <');
175-
176-
whereClause = whereClause
177-
.replace(/!=(?=(?:[^"']*"[^"']*"')*[^"']*$)/g, ' !=')
178-
.replace(/! =(?=(?:[^"']*"[^"']*"')*[^"']*$)/g, ' !=')
179-
.replace(/< =(?=(?:[^"']*"[^"']*"')*[^"']*$)/g, ' <=')
180-
.replace(/> =(?=(?:[^"']*"[^"']*"')*[^"']*$)/g, ' >=');
181-
182-
const parsedSQL = whereClause.split(' ');
183-
this.streamFields.forEach((field: any) => {
184-
parsedSQL.forEach((node: any, index: any) => {
185-
if (node === field.name) {
186-
node = node.replaceAll('"', '');
187-
parsedSQL[index] = '"' + node + '"';
188-
}
189-
});
190-
});
191-
192-
whereClause = parsedSQL.join(' ');
193-
194-
req.query.sql = req.query.sql.replace('[WHERE_CLAUSE]', ' WHERE ' + whereClause);
195-
} else {
196-
req.query.sql = req.query.sql.replace('[WHERE_CLAUSE]', '');
197-
}
128+
getDataProvider(
129+
type: SupplementaryQueryType,
130+
request: DataQueryRequest<MyQuery>
131+
): Observable<DataQueryResponse> | undefined {
132+
if (!this.getSupportedSupplementaryQueryTypes().includes(type)) {
133+
return undefined;
134+
}
198135

199-
req.query.sql = req.query.sql.replace('[INDEX_NAME]', queryData.stream);
200-
}
136+
switch (type) {
137+
case SupplementaryQueryType.LogsVolume:
138+
return this.getLogsVolumeDataProvider(request);
139+
default:
140+
return undefined;
141+
}
142+
}
201143

202-
req['encoding'] = 'base64';
203-
req.query.sql = b64EncodeUnicode(req.query.sql);
144+
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] {
145+
return [SupplementaryQueryType.LogsVolume];
146+
// return [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample];
147+
}
204148

205-
return req;
206-
} catch (e) {
207-
console.log('error in building query:', e);
208-
}
149+
getSupplementaryQuery(type: SupplementaryQueryType, query: MyQuery): MyQuery | undefined {
150+
return undefined;
209151
}
210-
updateStreamFields(streamFields: any[]) {
211-
this.streamFields = [...streamFields];
152+
153+
getLogsVolumeDataProvider(request: DataQueryRequest<MyQuery>): Observable<DataQueryResponse> | undefined {
154+
const logsVolumeRequest = cloneDeep(request);
155+
const targets = logsVolumeRequest.targets.map((target) => {
156+
target['refId'] = REF_ID_STARTER_LOG_VOLUME + target.refId;
157+
return target;
158+
});
159+
160+
if (!targets.length) {
161+
return undefined;
162+
}
163+
164+
return queryLogsVolume(
165+
this,
166+
{ ...logsVolumeRequest, targets },
167+
{
168+
extractLevel: () => LogLevel.unknown,
169+
range: logsVolumeRequest.range,
170+
targets: logsVolumeRequest.targets,
171+
}
172+
);
212173
}
213174
}

0 commit comments

Comments
 (0)