diff options
author | Erlend <erlendniko@hotmail.com> | 2022-07-28 11:07:56 +0200 |
---|---|---|
committer | Erlend <erlendniko@hotmail.com> | 2022-07-28 11:07:56 +0200 |
commit | 076635da2ea27d938180076476d43c33ea86ff51 (patch) | |
tree | 5cdf49379ece31f91958954b5f053f9f3ff1383b /client/js | |
parent | 4c235d3b8db1b046c20a1d8f75f8fb9e6e28b4fe (diff) |
Added ability to download trace from response for use in visualization
Diffstat (limited to 'client/js')
7 files changed, 285 insertions, 11 deletions
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx new file mode 100644 index 00000000000..3997c9c4bcc --- /dev/null +++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx @@ -0,0 +1,44 @@ +import React, { useContext } from 'react'; +import { ResponseContext } from '../Contexts/ResponseContext'; +import transform from '../../TransformVespaTrace'; +import SimpleButton from './SimpleButton'; + +export default function DownloadJSONButton() { + const { response } = useContext(ResponseContext); + + const transformResponse = (response) => { + return transform(response); + }; + + const handleClick = () => { + if (response != '') { + let transformedResponse = JSON.stringify( + transformResponse(JSON.parse(response), undefined, '\t') + ); + // taken from safakeskinĀ“s answer on SO, link: https://stackoverflow.com/questions/55613438/reactwrite-to-json-file-or-export-download-no-server + const fileName = 'vespa-response'; + const blob = new Blob([transformedResponse], { + type: 'application/json', + }); + const href = URL.createObjectURL(blob); + + // create "a" HTLM element with href to file + const link = document.createElement('a'); + link.href = href; + link.download = fileName + '.json'; + document.body.appendChild(link); + link.click(); + + // clean up "a" element & remove ObjectURL + document.body.removeChild(link); + URL.revokeObjectURL(href); + window.open('http://localhost:16686/search', '__blank'); + } else { + alert('Response was empty'); + } + }; + + return ( + <SimpleButton onClick={handleClick}>Download response as JSON</SimpleButton> + ); +} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx index 064bf51795b..7126a472d55 100644 --- a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx +++ b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx @@ -40,6 +40,7 @@ export const QueryInputProvider = (prop) => { recall: { name: 'recall', type: 'List', hasChildren: false }, user: { name: 'user', type: 'String', hasChildren: false }, metrics: { name: 'metrics', type: 'Parent', hasChildren: true }, + trace: { name: 'trace', type: 'Parent', hasChildren: true }, }; // Children of the levelZeroParameters that have child attributes diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx index 65e32945c55..bd41edd6008 100644 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx +++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx @@ -8,7 +8,7 @@ import AddPropertyButton from '../Buttons/AddPropertyButton'; import QueryInputChild from './QueryInputChild'; export default function QueryInput() { - const { inputs, setInputs, levelZeroParameters, childMap } = + const { inputs, setInputs, levelZeroParameters } = useContext(QueryInputContext); function removeRow(id) { diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx index d10935bf4c9..4ab2a074214 100644 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx +++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx @@ -94,7 +94,9 @@ export default function QueryInputChild({ id }) { const inputList = childArray.map((child) => { return ( <div key={child.id} id={child.id}> - {child.id == '4.1' && console.log(child.type)} + { + //child.id == '4.1' && console.log(child.type) + } <QueryDropdownForm choices={childMap[currentTypes]} id={child.id} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx index 9ee370de7c9..a3714f27fb5 100644 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx +++ b/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx @@ -1,10 +1,10 @@ import React, { useContext, useEffect, useState } from 'react'; -import SimpleDropDownForm from './SimpleDropDownForm'; import SimpleButton from '../Buttons/SimpleButton'; -import SimpleForm from './SimpleForm'; import { QueryInputContext } from '../Contexts/QueryInputContext'; import { ResponseContext } from '../Contexts/ResponseContext'; import { QueryContext } from '../Contexts/QueryContext'; +import SimpleForm from './SimpleForm'; +import SimpleDropDownForm from './SimpleDropDownForm'; export default function SendQuery() { const { inputs } = useContext(QueryInputContext); @@ -65,15 +65,12 @@ export default function SendQuery() { case 'Integer': case 'Long': return parseInt(input); - break; case 'Float': return parseFloat(input); - break; case 'Boolean': return input.toLowerCase() === 'true' ? true : false; - break; default: return input; diff --git a/client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx b/client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx new file mode 100644 index 00000000000..e6f89a39be4 --- /dev/null +++ b/client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx @@ -0,0 +1,230 @@ +let traceID = ''; +let processes = {}; +let output = {}; +let traceStartTimestamp = 0; +let topSpanId = ''; +let parentID = ''; + +// Generates a random hex string of size "size" +const genRanHex = (size) => + [...Array(size)] + .map(() => Math.floor(Math.random() * 16).toString(16)) + .join(''); + +export default function transform(trace) { + traceID = genRanHex(32); + output = { data: [{ traceID: traceID, spans: [], processes: {} }] }; + let data = output['data'][0]['spans']; + processes = output['data'][0]['processes']; + processes.p0 = { serviceName: 'Query', tags: [] }; + let temp = trace['trace']['children']; + let spans = findChildren(temp); + let query = findChildren(spans); // only used for getting the start time + traceStartTimestamp = getTraceStartTime(query); + let totalTraceDuration = spans[spans.length - 1]['timestamp'] * 1000; + topSpanId = genRanHex(16); + let topSpan = { + traceID: traceID, + spanID: topSpanId, + operationName: 'Complete', + startTime: traceStartTimestamp, + duration: totalTraceDuration, + references: [], + tags: [], + logs: [], + processID: 'p0', + }; + data.push(topSpan); + + const retrieved = findLogsAndChildren(spans, topSpan); + const logs = retrieved['logs']; + const children = retrieved['children']; + traverseLogs(logs); + createChildren(children); + + return output; +} + +function findLogsAndChildren(spans, topSpan) { + let logs = []; + let children = []; + for (let span of spans) { + if (span.hasOwnProperty('children')) { + let log = []; + for (let x of span['children']) { + if (Array.isArray(x['message'])) { + children.push(x['message']); + } else { + log.push(x); + } + } + logs.push(log); + } else if (span.hasOwnProperty('message')) { + topSpan['logs'].push({ + timestamp: traceStartTimestamp + span['timestamp'] * 1000, + fields: [{ key: 'message', type: 'string', value: span['message'] }], + }); + } + } + return { logs: logs, children: children }; +} + +function traverseLogs(logs) { + let first = true; + let data = output['data'][0]['spans']; + for (let log of logs) { + let logStartTimestamp = traceStartTimestamp + log[0]['timestamp'] * 1000; + let logDuration = + (log[log.length - 1]['timestamp'] - log[0]['timestamp']) * 1000; + let spanID = genRanHex(16); + if (first) { + parentID = spanID; + first = false; + } + let childSpan = { + traceID: traceID, + spanID: spanID, + operationName: 'test', + startTime: logStartTimestamp, + duration: logDuration, + references: [ + { refType: 'CHILD_OF', traceID: traceID, spanID: topSpanId }, + ], + tags: [], + logs: [], + processID: 'p0', + }; + data.push(childSpan); + for (let logPoint of log) { + if (logPoint.hasOwnProperty('message')) { + childSpan['logs'].push({ + timestamp: traceStartTimestamp + logPoint['timestamp'] * 1000, + fields: [ + { key: 'message', type: 'string', value: logPoint['message'] }, + ], + }); + } + } + } +} + +function createChildren(children) { + for (let i = 0; i < children.length; i++) { + let child = children[i][0]; + let processKey = `p${i + 1}`; + processes[processKey] = { serviceName: `Span${i}`, tags: [] }; + let spanID = genRanHex(16); + let data = output['data'][0]['spans']; + let startTimestamp = Date.parse(child['start_time']) * 1000; + let newSpan = { + traceID: traceID, + spanID: spanID, + operationName: `query${i}`, + startTime: startTimestamp, + duration: child['duration_ms'] * 1000, + references: [{ refType: 'CHILD_OF', traceID: traceID, spanID: parentID }], + tags: [], + logs: [], + processID: processKey, + }; + data.push(newSpan); + let traces = child['traces']; + for (let k = 0; k < traces.length; k++) { + let trace = traces[k]; + let traceTimestamp = trace['timestamp_ms']; + let events; + let firstEvent; + let duration; + if (trace['tag'] === 'query_execution') { + events = trace['threads'][0]['traces']; + firstEvent = events[0]; + duration = (traceTimestamp - firstEvent['timestamp_ms']) * 1000; + } else if (trace['tag'] === 'query_execution_plan') { + events = []; + let nextTrace = traces[k + 1]; + firstEvent = trace; + // query execution plan has no events, duration must therefore be found using the next trace + if (nextTrace['tag'] === 'query_execution') { + duration = + (nextTrace['threads'][0]['traces'][0]['timestamp_ms'] - + traceTimestamp) * + 1000; + } else { + duration = (nextTrace['timestamp_ms'] - traceTimestamp) * 1000; + } + } else { + events = trace['traces']; + firstEvent = events[0]; + duration = (traceTimestamp - firstEvent['timestamp_ms']) * 1000; + } + let childSpanID = genRanHex(16); + let childSpan = { + traceID: traceID, + spanID: childSpanID, + operationName: trace['tag'], + startTime: startTimestamp + firstEvent['timestamp_ms'] * 1000, + duration: duration, + references: [{ refType: 'CHILD_OF', traceID: traceID, spanID: spanID }], + tags: [], + logs: [], + processID: processKey, + }; + data.push(childSpan); + if (events.length > 0) { + for (let j = 0; j < events.length; j++) { + let event = events[j]; + let eventID = genRanHex(16); + let eventStart = event['timestamp_ms']; + let operationName; + if (event.hasOwnProperty('event')) { + operationName = event['event']; + if ( + operationName === 'Complete query setup' || + operationName === 'MatchThread::run Done' + ) { + duration = (traceTimestamp - eventStart) * 1000; + } else { + duration = (events[j + 1]['timestamp_ms'] - eventStart) * 1000; + } + } else { + operationName = event['tag']; + duration = (events[j + 1]['timestamp_ms'] - eventStart) * 1000; + } + let eventSpan = { + traceID: traceID, + spanID: eventID, + operationName: operationName, + startTime: startTimestamp + eventStart * 1000, + duration: duration, + references: [ + { refType: 'CHILD_OF', traceID: traceID, spanID: childSpanID }, + ], + tags: [], + logs: [], + processID: processKey, + }; + data.push(eventSpan); + } + } + } + } +} + +function findChildren(traces) { + for (let trace of traces) { + if (trace.hasOwnProperty('children')) { + return trace['children']; + } + } +} + +// Get an estimated start time by using the start time of the query and subtracting the current run time +function getTraceStartTime(trace) { + for (let x of trace) { + if (Array.isArray(x['message'])) { + let timestamp = Date.parse(x['message'][0]['start_time']) * 1000; + let currentTimestamp = x['timestamp'] * 1000; + return timestamp - currentTimestamp; + } + } +} diff --git a/client/js/app/src/app/pages/querybuilder/query-builder.jsx b/client/js/app/src/app/pages/querybuilder/query-builder.jsx index ebe8b759fa4..465720595f7 100644 --- a/client/js/app/src/app/pages/querybuilder/query-builder.jsx +++ b/client/js/app/src/app/pages/querybuilder/query-builder.jsx @@ -6,15 +6,14 @@ import { QueryInputProvider } from './Components/Contexts/QueryInputContext'; import SendQuery from './Components/Text/SendQuery'; import { ResponseProvider } from './Components/Contexts/ResponseContext'; import ResponseBox from './Components/Text/ResponseBox'; - -import '../../styles/agency.css'; -import '../../styles/vespa.css'; import ShowQueryButton from './Components/Buttons/ShowQueryButton'; import { QueryProvider } from './Components/Contexts/QueryContext'; import PasteJSONButton from './Components/Buttons/PasteJSONButton'; import CopyResponseButton from './Components/Buttons/CopyResponseButton'; +import DownloadJSONButton from './Components/Buttons/DownloadJSONButton'; -//import 'bootstrap/dist/css/bootstrap.min.css'; //TODO: Find out how to get this css +import '../../styles/agency.css'; +import '../../styles/vespa.css'; export function QueryBuilder() { return ( @@ -43,6 +42,7 @@ export function QueryBuilder() { <TextBox className="response">Response</TextBox> <ResponseBox /> <CopyResponseButton /> + <DownloadJSONButton /> </ResponseProvider> <br /> <br /> |